CI Location Field Script
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
3 weeks ago
I have a question regarding the location field for some of my company's Configuration Items (CIs) under the `cmdb_ci_list`. I posted about this issue on the ServiceNow Developer Forum [here](https://www.servicenow.com/community/developer-forum/the-location-field-for-some-of-our-cis-is-wrong...). Specifically, I discovered a scheduled job that updates the CI location based on the IP address (see "Location Issue-14"). In this job, there is a script (see "Location Issue-15").
I am considering updating the scheduled job with a new script. I would appreciate any thoughts on whether this new script will accomplish the task. Here’s the script:
// Helper function to safely convert IP to an unsigned 32-bit integer
function ipToInt(ip) {
if (!ip) return -1;
var parts = ip.trim().split('.');
if (parts.length !== 4) return -1;
// Using >>> 0 forces JavaScript to treat the result as an unsigned 32-bit integer
return ((parseInt(parts[0], 10) << 24) |
(parseInt(parts[1], 10) << 16) |
(parseInt(parts[2], 10) << 8) |
parseInt(parts[3], 10)) >>> 0;
}
var updatedCount = 0;
// 1. Caching ranges in memory avoids running a nested loop query for every single CI
var ranges = [];
var rangeGR = new GlideRecord('ip_address_range');
rangeGR.query();
while (rangeGR.next()) {
var startInt = ipToInt(rangeGR.start_ip + '');
var endInt = ipToInt(rangeGR.end_ip + '');
if (startInt !== -1 && endInt !== -1) {
ranges.push({
start: startInt,
end: endInt,
location: rangeGR.getValue('u_location_info') // Use getValue for references
});
}
}
// 2. Query Configuration Items needing evaluation
var ciGR = new GlideRecord('cmdb_ci');
ciGR.addNotNullQuery('ip_address');
// Optional: restrict query to only look at 10.14.x.x addresses for testing
// ciGR.addQuery('ip_address', 'STARTSWITH', '10.14.');
ciGR.query();
while (ciGR.next()) {
var ciIp = ciGR.ip_address + '';
var ciIpInt = ipToInt(ciIp);
if (ciIpInt === -1) {
gs.warn('[CI Location Update] Invalid IP on CI: ' + ciGR.getValue('name') + ' IP: [' + ciIp + ']');
continue;
}
var matched = false;
// Evaluate against the pre-loaded memory array
for (var i = 0; i < ranges.length; i++) {
var range = ranges[i];
if (ciIpInt >= range.start && ciIpInt <= range.end) {
matched = true;
var newLocation = range.location;
if (ciGR.getValue('location') != newLocation) {
ciGR.setValue('location', newLocation);
ciGR.update();
updatedCount++;
gs.info('[CI Location Update] CI "' + ciGR.getValue('name') + '" updated to location reference: ' + newLocation);
}
break; // Exit the loop once the first matching range is found
}
}
if (!matched) {
gs.info('[CI Location Update] No range match found for CI: ' + ciGR.getValue('name') + ' IP: ' + ciIp);
}
}
gs.info('[CI Location Update] Execution complete. Total CIs updated: ' + updatedCount);
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 weeks ago - last edited 2 weeks ago
Hi @BenFan,
I looked at this purely as the script you posted here, I don't have access to the "Location Issue-14/15" screenshots from your earlier thread, so I can't diff it line by line against the job you're replacing. But taken on its own, this is a solid improvement over the version of this pattern that usually gets recommended for IP-to-location matching. A few things are genuinely fixed, and a few things are worth verifying before you push it to your scheduled job.
The IP-to-integer conversion is correct, and that's not a given. The most common version of this pattern in the community (see this "find which range an IP is in" thread) builds the integer with + instead of |, roughly (o1 << 24) + (o2 << 16) + .... That breaks for any octet of 128 or higher, because shifting a value like 192 left by 24 sets the sign bit, and JavaScript's bitwise operators work on signed 32-bit integers, so the result goes negative. Once that happens, your >= / <= range comparisons silently misbehave across the 128 boundary, which covers most private ranges people actually use (172.x, 192.x). Your version combines the octets with | and forces the result unsigned with >>> 0, which avoids that bug entirely. Keep that part exactly as written.
Caching the ranges into an in-memory array before looping the CIs is also the right call. That turns a query-per-CI pattern into one query for ranges plus one query for CIs, which is a real performance win, not a cosmetic one.
Before you run this against production, I'd verify three things that live outside the script itself:
- Confirm u_location_info on ip_address_range is genuinely a reference field pointing at cmn_location. The location field on cmdb_ci is a reference to cmn_location, so setValue('location', newLocation) needs the sys_id of a real cmn_location record. If u_location_info turns out to be a string field or references a different table, the script will run without erroring, but the CI's location will render blank in the UI because the stored value won't resolve to anything, and it'll look like the job silently did nothing.
- Add an orderBy to the rangeGR query. Right now the ranges are evaluated in whatever order the database happens to return them, not by specificity. If any ranges overlap in your data, the break after the first match makes the outcome nondeterministic, since the "winning" range depends on row order rather than which range is actually the best fit. If overlaps are possible, sort narrowest-range-first (or by an explicit priority field) before the loop.
- Check how reliably cmdb_ci.ip_address is kept current on the CIs in scope. ServiceNow's own guidance in KB2583244 is that the single ip_address attribute on the base CI record is unreliable for anything with more than one network interface, since it can only hold one value at a time. If any CIs in scope are multi-homed, matching against that one field can put a CI into the wrong subnet's range even though the range-matching logic itself is fine. That may be closer to the actual root cause behind your original "location is wrong" thread than anything in this script.
None of those are bugs in the script as written, they're data-integrity assumptions the script depends on. If those three check out in your instance, this version should behave correctly and outperform whatever nested-query approach you're replacing.
Thank you,
Vikram Karety
Octigo Solutions INC
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 weeks ago
Hi @Vikram Reddy ,
Here is my concern regarding the newly scheduled job script. I am realizing the issue I am encountering is that I have a location with an IP range of 10.14.6.x and a second location with an IP range of 10.14.106.x; the IP range 10.14.6.x is being seen as overlapping with 10.14.106.x.
If I update my currently scheduled job script, what happens if I encounter an IP range that overlaps with another? Do I keep adding overlapping IP ranges to the ciGr variable?
var ciGR = new GlideRecord('cmdb_ci');
ciGR.addNotNullQuery('ip_address');
// Optional: restrict query to only look at 10.14.x.x addresses for testing
// ciGR.addQuery('ip_address', 'STARTSWITH', '10.14.');
ciGR.query();
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 weeks ago
Hi @BenFan,
Before you touch ciGR at all, go look at how the range check itself compares the two subnets, because that snippet you posted has nothing to do with the overlap. ciGR is just pulling every cmdb_ci record that has a populated ip_address, it doesn't hold or evaluate any ranges. The overlap logic lives somewhere else in the job, and that's where 10.14.6.x and 10.14.106.x are getting confused.
The classic cause of exactly this symptom is comparing IP addresses as plain strings instead of numbers. If the range boundaries or the match test use STARTSWITH, indexOf, or a straight string comparison on the octets, dotted-decimal strings don't sort the way you'd expect. "10.14.106.5" and "10.14.6.0" share the prefix "10.14." and then diverge at the third octet, but as text, "1" (the start of 106) is lexicographically less than "6", so "10.14.106.5" evaluates as less than "10.14.6.0" even though 106 is obviously bigger than 6 numerically. Depending on how the comparison is structured, that's enough to make the script think an address in the 106 subnet falls inside the 6 subnet's boundaries, and vice versa. It's a very common gotcha and it's almost always the culprit when two ranges that shouldn't touch start "overlapping."
The fix is to stop comparing strings and convert every address, and every range boundary, to a single 32-bit integer before you compare anything. Standard dotted-decimal to integer math:
ipInt = (octet1 * 16777216) + (octet2 * 65536) + (octet3 * 256) + octet4
Do that conversion once for the CI's ip_address, and once for each range's start and end address, then your match test is just ipInt >= rangeStart && ipInt <= rangeEnd. Once everything is numeric, 10.14.6.0 through 10.14.6.255 and 10.14.106.0 through 10.14.106.255 are two cleanly separated integer bands with a huge gap between them, and the false overlap disappears entirely.
To your actual question: no, don't keep appending overlapping ranges into ciGR, that variable was never the place for range definitions and stuffing more conditions into it won't fix a comparison bug upstream. Keep your ranges in their own array or table (whatever cmn_location field or custom table you're sourcing them from), do the integer conversion on that list once, and then loop the CIs against it. If you ever do have genuinely overlapping ranges by design, like a broad corporate range with a more specific building-level subnet carved out of it, sort your range list so the narrowest (most specific) range is evaluated first and break on first match, similar to longest-prefix-match logic in routing. That way a real overlap resolves predictably instead of depending on whatever order the ranges happen to be queried in.
If you can paste the actual overlap/range-matching function (not just the ciGR query), I'm happy to take a closer look, since the fix really depends on exactly how those ranges are stored and compared today.
Thank you,
Vikram Karety
Octigo Solutions INC
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
2 weeks ago
So we currently don't have a way to check any subnets, just a scheduled job script that compares the IPs as strings. See iprange-0; our IP range in the MID server doesn't specify specific ranges either.
function ipToInt(ip) {
if (!ip) return -1;
var parts = ip.trim().split('.');
if (parts.length !== 4) return -1;
return (parseInt(parts[0]) << 24) +
(parseInt(parts[1]) << 16) +
(parseInt(parts[2]) << 8) +
parseInt(parts[3]);
}
var updatedCount = 0;
var excludedClasses = [
'cmdb_ci_application',
'cmdb_ci_computer',
'cmdb_ci_esx_server',
'cmdb_ci_linux_server',
'cmdb_ci_server',
'cmdb_ci_win_server'
];
var ciGR = new GlideRecord('cmdb_ci');
ciGR.addNotNullQuery('ip_address');
ciGR.addQuery('sys_class_name', 'NOT IN', excludedClasses.join(','));
ciGR.query();
while (ciGR.next()) {
var ciIp = ciGR.ip_address + '';
var ciIpInt = ipToInt(ciIp);
if (ciIpInt === -1) {
gs.warn('[CI Location Update] Invalid IP on CI: ' + ciGR.name + ' IP: [' + ciIp + ']');
continue;
}
var rangeGR = new GlideRecord('ip_address_range');
rangeGR.query();
var matched = false;
while (rangeGR.next()) {
var startInt = ipToInt(rangeGR.start_ip + '');
var endInt = ipToInt(rangeGR.end_ip + '');
if (startInt === -1 || endInt === -1) continue;
if (ciIpInt >= startInt && ciIpInt <= endInt) {
matched = true;
var newLocation = rangeGR.u_location_info;
if (ciGR.location != newLocation) {
ciGR.location = newLocation;
ciGR.update();
updatedCount++;
gs.info('[CI Location Update] CI "' + ciGR.name + '" updated with location: ' + newLocation.getDisplayValue());
} else {
gs.info('[CI Location Update] CI "' + ciGR.name + '" already has correct location.');
}
break;
}
}
if (!matched) {
gs.info('[CI Location Update] No range match for CI: ' + ciGR.name + ' IP: ' + ciIp);
}
}
gs.info('[CI Location Update] Total CIs updated: ' + updatedCount);
Let me convert the addresses and all range boundaries into a single 32-bit integer and get back to you.