Some PDIs are currently unavailable, and PDI actions are paused. View the latest updates here. Read More

CI Location Field Script

BenFan
Mega Guru

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);

 

14 REPLIES 14

Hi @BenFan,

 

Run the updated job against a CI whose IP starts with 128 or higher before calling this fixed, that's the exact range the old bitwise version broke.

  • High-octet IPs: pick a CI in 192.168.x.x or 172.16-31.x.x that mismatched before, rerun, and confirm ciIpInt now comes back as a large positive number instead of a negative one.
  • ip_address_range bounds: start_ip and end_ip run through the same ipToInt call, so they inherit the fix automatically, just make sure there isn't a second copy of the old shift-based function sitting in a Script Include or another scheduled job elsewhere in the instance.
  • ip_address format: the function assumes exactly four dot-separated octets. A CIDR suffix, stray whitespace, or a multi-value IP field pushes parts.length past 4, which silently returns -1 and skips the CI. Watch the gs.warn "Invalid IP" output for those.
  • rangeGR query: it currently re-queries ip_address_range from scratch inside the outer while loop for every single CI. Not wrong, but worth caching into an array once above the loop if that table has any real size to it.

The arithmetic approach itself is sound, since JS numbers are double precision, multiplying out the octets instead of shifting bits sidesteps the 32-bit signed overflow completely.

 

Thank you,
Vikram Karety
Octigo Solutions INC

We don't have any IPs with a first octet higher than 10.

Hi @BenFan,

 

 The sign flip I described only happens because of how the shift lands: octet1 shifted left 24 bits puts that octet's own bit 7 directly on bit 31, the sign bit of a 32-bit signed integer. Only when the first octet is 128 or higher is that bit set. A 10.x.x.x network never sets it, no matter what the other three octets are. So on your instance, the old bitwise ipToInt and the new multiplication-based one would have returned the exact same positive number for every CI you have. That bug literally could not have produced the location mismatches you were originally chasing.

Which means the real cause is one of the other things I flagged, not the overflow. Given you're on a flat 10.0.0.0/8 range, here's where I'd actually look:

  • Format parsing: if the CI's ip_address value has a CIDR suffix, a trailing space, or more than one address crammed into the field (Discovery does this on multi-homed hosts), parts.length comes back over 4 and the function returns -1 and skips the CI silently. On a large 10.x.x.x estate pulled in by Discovery this is honestly the most common offender. Turn on the gs.warn "Invalid IP" line and watch it for a run, don't just eyeball the CI list.
  • Range boundary logic: test a CI whose ip_address is exactly equal to a range's start_ip or end_ip. If the comparison in the while loop is a strict < or > instead of <= or >=, edge-of-range CIs get excluded even though the math is otherwise correct.
  • parseInt without a radix: ServiceNow's server-side scripts run on Rhino, and older Rhino/pre-ES5 behavior treats a numeric string starting with a leading zero as octal unless you pass a radix. If any octet in your data ever gets stored with a leading zero, parseInt(octet) can quietly return the wrong number instead of throwing anything. Cheap insurance, just always call parseInt(octet, 10).

Fastest way to actually pin it down on your data: pull two or three CIs that are still mismatching and temporarily log the three numbers instead of guessing.

gs.info('CI: ' + ci.ip_address + ' ciIpInt=' + ciIpInt + ' start=' + startInt + ' end=' + endInt);

If ciIpInt comes back as -1, it's the format/parts.length issue. If it's a real positive number but just outside the range by a hair, it's the boundary comparison. If it's flat out wrong for a CI you know is in range, check for a leading-zero octet or a stray non-numeric character getting through.

 

Thank you,
Vikram Karety
Octigo Solutions INC

If the fastest way includes adding

'CI: ' + ci.ip_address + ' ciIpInt=' + ciIpInt + ' start=' + startInt + ' end=' + endInt

In gs.info(), which line should I update? Is it line 56, 58, 66, 70, or all lines that call gs.info()? Location Issue-20.pngWhen I temporarily track three IPs with incorrect locations, should I run the script first and then check the three IPs in a day? Location Issue-18.png

 

Hi @Vikram Reddy,


Just following up on your response, I wanted to clarify about updating the gs.info() method to include:

'CI: ' + ci.ip_address + ' ciIpInt=' + ciIpInt + ' start=' + startInt + ' end=' + endInt

Which line should this be updated on? Is it line 56, 58, 66, 70, or all lines that call gs.info()?