Name
global.DiscoverySensor
Description
The default sensor object. All sensors should extend this object.
Script
var DiscoverySensor = Class.create();
DiscoverySensor.HistoryStates = {
ALIVE_NOT_ACTIVE: 'Alive, not active, not classified',
ACTIVE_COULDNT_CLASSIFY: 'Active, couldn\'t classify',
CLASSIFIED_BUT_DISABLED: 'Classified, but disabled'
};
DiscoverySensor.DeviceStates = {
REJECTED: 'rejected'
};
DiscoverySensor.PayloadErrors = [
'Adding target to blacklist',
'Target is blacklisted',
'Cannot locate config file:',
'Failed to authenticate',
'bash: haproxy: command not found'
];
var sourceName = gs.getProperty('glide.discovery.source_name', "ServiceNow");
DiscoverySensor.prototype = {
initialize: function(definition) {
var fSW, msg, trace, i;
try {
fSW = new GlideStopWatch();
this.init(definition);
fSW.stop();
this._recordStopWatch("Initialization", fSW.getTime());
this.main();
} catch (e) {
if (e instanceof DiscoveryException)
throw e;
var errMsg = 'Script error in sensor: ' + e;
DiscoveryLogger.warn(errMsg, 'Discovery Sensor', this.getEccQueueId(), null);
if (g_device)
g_device.issue();
// Re-throw it for logging the stack trace in the system log
if (e instanceof Packages.java.lang.Throwable) {
msg = [ '-----------------------------\nStack:' ];
trace = e.getStackTrace();
for (i = 0; i < trace.length; i++)
msg.push(trace[i]);
msg.push('Message:');
var lastCause;
while (e && (lastCause != e)) {
lastCause = e;
try {
msg.push(e.getMessage());
e = e.getCause();
e && msg.push('Caused by:');
} catch (e2) { }
}
msg.push('-----------------------------');
throw (msg.join('\n'));
} else {
if (e.stack)
throw (e + '\nStack:' + e.stack);
else {
// We no longer get stacks :-(
msg = [ "Caught Exception. Name: '" + e.name + "', Message: '" + e.message + "'" ];
if (e.fileName || e.sourceName)
msg.push('from file: ' + (e.fileName || e.sourceName) + ', line ' + e.lineNumber);
throw msg.join('\n');
}
}
} finally {
fSW = new GlideStopWatch();
this.updateCounts();
fSW.stop();
this._recordStopWatch("Update count", fSW.getTime());
}
},
init: function(definition) {
this.cache = {};
this.relatedListdataObj = {};
this.errorMap = {};
this.warningMap = {};
this.errorManager = new SNC.DiscoveryErrorManager();
this._defaultSave = true;
this._debug = null; // lazy-loaded by isDebugging()
this._deviceCi = null; // lazy-loaded by getDeviceCi()
this._ciSchema = null; // lazy-loaded by getCiSchema()
// added to differentiate if application pattern gets triggered for Agent-based Discovery(ACC) vs regular Discovery flow
this.accAgentId = null; // must remain null for regular Discovery flow
for (var property in definition)
this[property] = definition[property];
this.ASensor = g_sensor;
g_sensor = this;
g_status = new DiscoveryStatus(this.getAgentCorrelator());
// initialize ciData, filling it with 'current' if possible
// and then overriding with the 'cidata' parameter, if possible
this.ciData = new CIData();
this.ci_data = this.ciData.data; // deprecate
var cidataXml = this.getParameter('cidata'); // if provided, overwrite
if (!gs.nil(cidataXml))
this.ciData.fromXML(cidataXml);
if (!this.hasOwnProperty('type')) // use the sensor name as the type if not specified
this.type = this.getSensorName();
},
main: function() {
// The idea here is that we do not lock on a CI until we have a sys_id. Currently shazzam, classify and
// identity sensors will not have them. When shazzam runs, it would not have any device history record.
if (JSUtil.nil(g_device)){
return this._main();
}
// When classify or identity sensors are running, it has no CI in device history record.
var ciSysID = JSUtil.notNil(g_device.getSourceCmdbCi()) ? g_device.getSourceCmdbCi() : this.getParameter('cmdb_ci');
if (JSUtil.nil(ciSysID)){
return this._main();
}
// Make a special exception for ADM sensors
if (this.getSensorName().endsWith("ADM")){
return this._main();
}
// It turns out that sensors should update fields that are related to its area, so we shoudln't need to lock as it causes
// a lot of database churn in the sys_mutex table.
var sensorLock = JSUtil.toBoolean(gs.getProperty("glide.discovery.sensor.ci_lock", "false"));
if (!sensorLock) {
this._main();
return;
}
var rl = new GlideRecordLock('cmdb_ci', ciSysID);
// limit our attempt to get a lock to 120 seconds.
rl.setSpinWait(300);
rl.setMaxSpins(400);
rl.setLockDuration(120); //seconds
if (!rl.get()) {
// TODO: Somehow re-schedule this job to a later date in the future instead of just failing.
this.ASensor.setProcessed(false);
this.ASensor.setError("Failed to obtain a lock on cmdb_ci: " + ciSysID + " to process the sensor");
return;
}
try {
this._main();
} finally {
rl.release();
}
},
_main: function() {
var fSW = new GlideStopWatch();
this.run();
fSW.stop();
this._recordStopWatch("Script processing", fSW.getTime());
fSW = new GlideStopWatch();
this.save();
fSW.stop();
this._recordStopWatch("Database update", fSW.getTime());
fSW = new GlideStopWatch();
this.after();
fSW.stop();
this._recordStopWatch("After method", fSW.getTime());
},
isClassificationProbe: function() {
var useClassParameter = this.getParameter('use_class') || '';
return JSUtil.notNil(useClassParameter);
},
run: function() {
var results = g_array_util.ensureArray(document.result);
// if there's an error attribute in the "results" tag, then log it...
if (document['@error']) {
// This is the overall error from the probe. It's possible for each individual result to
// have different errors. Generally they don't and we set the probe's error and each
// result's error to the same value. Here we save the value of the probe's error. Later,
// when reporting a result's error we'll compare and _not_ report it if it's a duplicate.
this.errorFromResults = '' + document['@error'];
this.processError({msg: this.errorFromResults, resultCode: '' + document['@result_code']});
}
//This is the case where we only get <result/> and nothing else. Windows probes could return this.
else if (results.length === 0)
this.zeroResults();
this.prepare(results);
for (var i = 0; i < results.length; i++) {
var result = results[i];
// Checks for warnings and log them
this.logWarningPayload(result);
// Checks for debug info and log them
this.logDebugInfoPayload(result);
// Checks for error and handles it by printing out log msgs
if (this.checkErrorPayload(result) && (""+this.getParameter('debug')).toLowerCase() != "true") {
this.setTriggerProbes(false); // Prevent additional probes trigger if error is encountered.
if (this.isClassificationProbe())
this.processDupIpRecords();
continue;
}
// This checks the payload in more detail to see if we really have something to process.
if (this.checkEmptyPayload(result)) {
this.processError('No result returned from probe.');
continue;
}
this.process(result);
}
this.finish();
},
getDuplicateIpMutex: function(status, ci) {
var mutexName = "DeviceDuplicateIp_" + status + "_" + ci;
return this.getMutex(mutexName);
},
getClassificationMutex: function(status, source) {
var mutexName = "ClassificationProbe_" + status + "_" + source;
return this.getMutex(mutexName);
},
getMutex: function(mutexName) {
var mutex = new SelfCleaningMutex(mutexName, mutexName);
mutex.setExclusive(true);
mutex.setSpinWait(100);
mutex.setMutexExpires(120000);
if (!mutex.get())
throw 'Timed out getting mutex for ' + mutexName;
return mutex;
},
getProcessDuplicateIpRecord: function() {
var duplicateIpCurrentSourceGr = new GlideRecord('discovery_device_duplicate_ips');
duplicateIpCurrentSourceGr.addQuery('source', source);
duplicateIpCurrentSourceGr.addQuery('status', this.getAgentCorrelator());
duplicateIpCurrentSourceGr.addQuery('classification_probe', probe);
duplicateIpCurrentSourceGr.addQuery('state', 'process');
duplicateIpCurrentSourceGr.query();
return duplicateIpCurrentSourceGr;
},
/*
* This function is called when a classification fails
* Duplicate IP record is created before the sensor is running,
* so there is also a record created for this discovery
* We need to mark this record as an "error" and release one of the records
* that was not processed since it was marked as "skip"
*/
processDupIpRecords: function() {
var eccGr = new GlideRecord('ecc_queue');
var script = 'var job = new DiscoverySensorJob();\njob.process();';
var duplicateIpCurrentSourceGr = this.getProcessDuplicateIpRecord();
if (duplicateIpCurrentSourceGr.next()) {
if (JSUtil.nil(duplicateIpCurrentSourceGr.getValue('cmdb_ci'))) {
duplicateIpCurrentSourceGr.state = 'error';
duplicateIpCurrentSourceGr.update();
return;
}
var duplicateIpMutex = this.getDuplicateIpMutex(this.getAgentCorrelator(), duplicateIpCurrentSourceGr.getValue('cmdb_ci'));
try {
// need to recheck the state of the duplicate ip record after acquiring mutex to confirm the state
duplicateIpCurrentSourceGr = this.getProcessDuplicateIpRecord();
if (!duplicateIpCurrentSourceGr.next())
return;
duplicateIpCurrentSourceGr.state = 'error';
duplicateIpCurrentSourceGr.update();
/*
* Find any duplicate IP records for THIS CI that were skipped and re-schedule one of them
* The reason is that if 1 classification failed, that doesn't mean other
* classifications on other IPs belong to the same host may fail as well
* Different IPs can respond on different ports, hence needed to be checked in case
* of a failed classification
*/
var dupIpOtherSourceSameCiGr = new GlideRecord('discovery_device_duplicate_ips');
dupIpOtherSourceSameCiGr.addQuery('source', '!=', source);
dupIpOtherSourceSameCiGr.addQuery('cmdb_ci', duplicateIpCurrentSourceGr.getValue('cmdb_ci'));
dupIpOtherSourceSameCiGr.addQuery('status', this.getAgentCorrelator());
dupIpOtherSourceSameCiGr.addQuery('classification_probe', probe);
dupIpOtherSourceSameCiGr.addQuery('state', 'skip');
dupIpOtherSourceSameCiGr.query();
/*
* Change the state of the duplicate IP record to "process" and
* re-schedule the ECC queue that was skipped
*/
if (dupIpOtherSourceSameCiGr.next()) {
eccGr.get(dupIpOtherSourceSameCiGr.getValue('ecc_queue'));
dupIpOtherSourceSameCiGr.state = 'process';
dupIpOtherSourceSameCiGr.update();
new SchedulePriorityECCJob('ASYNC: Discovery - Sensors', eccGr, script).schedule();
}
} finally {
duplicateIpMutex.release();
}
}
},
/**
* Helper method to set output and related_data global variables for JSON processing
*/
prepareJSON: function(result) {
if (gs.nil(result.output))
return;
var validJsonPattern = new SNC.Regex(/^\s*\{.*\}\s*$/);
if (!validJsonPattern.match(result.output))
throw new DiscoveryException("Sensor is expecting JSON format in the output field after probe post processor script. Please check that your MID server is up to date.");
// Decode the json string to object
var jsonResult = new JSON().decode(result.output);
// Log anything we need to
if (jsonResult.logs) {
for (var i = 0; i < jsonResult.logs.length; i++) {
if (g_device != null) // Is this part of a Discovery Schedule?
g_device.log(jsonResult.logs[i], this.getSensorName());
else
gs.log(jsonResult.logs[i]);
}
}
// Auto map current and related_data
if (jsonResult.current)
current = jsonResult.current;
else
current = {};
// empty related list if not set
if (jsonResult.related_data)
related_data = jsonResult.related_data;
else
related_data = {};
},
/**
* Retrieves the Device History for the current device
*/
getDeviceHistory: function() {
return new DeviceHistoryJS();
},
/**
* Retrieves the deprecated Java DeviceHistory object.
*/
getDeviceHistoryGlobal: function() {
return g_device;
},
/**
* Provides a Ci object representing the current device CI under probe. Lazy-loaded.
* PRB916595 and PRB654648: To prevent over_writing some fields that are not updated by
* the same code called the following function we set non_system fields to"undefined"
* in the Ci object(not CI record in DB).
* NOTE: Using this function you can not read the non-system fields in the Ci object.
* EXAMPLE: Using the following code returns undefined instead of correct value
* var deviceCi = this.getDeviceCi();
* gs.print(deviceCi.data.name); // result will be undefined
* EXAMPLE: The function still can be used to read the system fields, fields with name
* starting with sys_
* var deviceCi = this.getDeviceCi();
* gs.print(deviceCi.data.sys_id); // result will be the sys_id of the device
* @return Ci
* @throws Error If unable to load from the database.
*
*/
getDeviceCi: function() {
if (this._deviceCi !== null) // already lazy-loaded
return this._deviceCi;
var ciGr = this.getCmdbRecord();
if (ciGr !== null){
this._deviceCi = this.getCiSchema().createCi(ciGr);
this._deviceCi.setDataUndefined();
} else
throw 'Unable to load CMDB record for CI';
if (this.isDebugging()) {
this._deviceCi.debug(true);
Debug.logObject('Device CI', this._deviceCi.toShallowObj());
}
return this._deviceCi;
},
/**
* Provided the Ci Schema for this sensor.
* May be overloaded per-sensor to handle custom CMDB configurations.
* @return CiSchema
*/
getCiSchema: function() {
if (this._ciSchema !== null) // already lazy-loaded
return this._ciSchema;
this._ciSchema = new CiSchema();
return this._ciSchema;
},
/*
* Override this in the sensor to change the behavior with no results.
*/
zeroResults: function() {
this.processError('No results returned from probe.');
},
/*
* Override this in the sensor to process each result.
*/
process: function(result) {
},
/*
* Optionally override this in the sensor to prepare for processing results. This method is invoked once before any
* results have been processed.
*/
prepare: function() {
},
/*
* Optionally override this in the sensor to finish after processing all results. This method is invoked once after
* all results have been processed.
*/
finish: function() {
},
/*
* Optionally override this in the sensor to finish after processing and saving all results. This method is invoked once after
* all results have been processed and saved.
*/
after: function() {
},
/*
* Optionally override this in the sensor to handle errors.
* @param [string] errors An array of all error messages.
* @param undefined|{sourceName: string, deviceState:undefined|string,lastState:undefined|string,fireClassifiers:undefined|boolean} config The config object, customizes non-default error handling.
*/
handleError: function(errors, config) {
if (typeof config === 'undefined')
config = {};
var sourceName = ( typeof config.sourceName === 'undefined' ? 'Discovery' : config.sourceName );
var deviceState = ( typeof config.deviceState === 'undefined' ? null : config.deviceState );
var lastState = ( typeof config.lastState === 'undefined' ? null : config.lastState );
var fireClassifiers = ( typeof config.fireClassifiers === 'undefined' ? false : config.fireClassifiers );
// warn on every error, but only customize the error message for the first error that we can
for (var i = 0; i < errors.length; ++i) {
DiscoveryLogger.warn(errors[i], sourceName, this.getEccQueueId());
this.handleItomError(errors[i]);
}
if (g_device) {
var logStateChanges = false;
var currentState = '';
if (lastState !== null) {
var status = new DiscoveryStatus(g_device.getStatus());
logStateChanges = status.logStateChanges;
}
if (this.shouldUpdateDeviceIssueState(errors))
g_device.changeDeviceIssueState(deviceState, lastState, currentState, logStateChanges, sourceName, this.getEccQueueId());
}
if (fireClassifiers && !this.firedAdditionalClassifier) {
this.firedAdditionalClassifier = true;
this.fireAdditionalClassifier(true); // skip setting a final error msg as we already have
}
},
shouldUpdateDeviceIssueState: function(errors) {
if (errors.length === 0)
return false;
if (errors.length !== 1)
return true;
if (!errors[0].msg)
return false;
/*
* When we are dealing with a classify probe we always want to update issue state so we
* don't leave a device in classifying state.
*/
if (JSUtil.notNil(g_probe.getParameter("use_class")))
return true;
/*
* Iterating over the errors to see whether we want to skip the update of device history.
* some() will return 'true' in case the condition is met on ANY of our values and in our case, if we find a match
* we want to return 'false' [hence the !] and if none of our errors was found in the message
* we want to return 'true' [= update device history record]
*/
return !DiscoverySensor.PayloadErrors.some(function(error) { return errors[0].msg.indexOf(error) > -1; });
},
shouldLogItomError: function(errorMsg) {
if (errorMsg == null)
return false;
if (!this.errorMsgBlacklistCache)
this.initErrorMsgBlacklistCache();
for (var item = 0; item < this.errorMsgBlacklistCache.length; item++) {
switch (this.errorMsgBlacklistCache[item].match_condition) {
case "Exact match":
if (this.errorMsgBlacklistCache[item].phrase.toLowerCase() === errorMsg.toLowerCase())
return false;
break;
case "Starts with":
if (errorMsg.toLowerCase().startsWith(this.errorMsgBlacklistCache[item].phrase.toLowerCase()))
return false;
break;
case "Contains":
if (errorMsg.toLowerCase().includes(this.errorMsgBlacklistCache[item].phrase.toLowerCase()))
return false;
break;
}
}
return true;
},
initErrorMsgBlacklistCache: function() {
this.errorMsgBlacklistCache = [];
var gr = new GlideRecord("automation_error_msg_blacklist");
gr.addQuery("active", "true");
gr.query();
while (gr.next())
this.errorMsgBlacklistCache.push({phrase: gr.getValue("phrase"), match_condition: gr.getValue("match_condition")});
},
handleItomError: function(error) {
var errorMsg = typeof error === 'object' && error.hasOwnProperty('msg') ? error.msg : error;
if (errorMsg.indexOf('Access is denied') !== -1 ||
errorMsg.indexOf('Authentication failure(s) with available Windows credentials') !== -1 )
errorMsg = 'Failed to access target system. Access is denied';
if (!this.shouldLogItomError(errorMsg))
return;
var itomErrorCode = 'SN-5999'; // Default: Unexpected error
var deviceClass = this.getOsForError();
if (errorMsg.indexOf('Cannot connect, status is') !== -1)
itomErrorCode = 'SN-1007';
else if ((errorMsg.indexOf('No credential found for type') != -1) ||
(errorMsg.indexOf('No valid credential found for type') != -1)) {
var credentialTypes = this.extractCredentialTypesFromMessage(errorMsg);
itomErrorCode =
DiscoveryErrorsUtilities.getFirstMatchedItomErrorCodeByCredentialTypes(
credentialTypes
);
}
else if (errorMsg.indexOf('Connection failed') !== -1)
itomErrorCode = 'SN-1006';
else if (errorMsg.indexOf('MID Server isn\'t Windows') !== -1)
itomErrorCode = 'SN-5602';
else if (errorMsg.indexOf('Cannot connect, status is') !== -1)
itomErrorCode = 'SN-1007';
else if (errorMsg.indexOf('Payload length') !== -1)
itomErrorCode = 'SN-5400';
else if (errorMsg.indexOf('SNMP probe timed out. Target is either unreachable') !== -1)
itomErrorCode = 'SN-1026';
else if (errorMsg.indexOf('Failed to authenticate with credentials') !== -1)
itomErrorCode = 'SN-1100';
else if (errorMsg.indexOf('Session has timed out') !== -1)
itomErrorCode = 'SN-5701';
else if (errorMsg.indexOf('Access is denied') !== -1)
itomErrorCode = 'SN-1099';
var ciSysId = this.getCiSysID();
if (ciSysId == "")
ciSysId = null;
this.errorManager.addError(new SNC.DiscoveryErrorMsg(itomErrorCode, this.getSource(), this.getAgentCorrelator(), ciSysId, errorMsg, deviceClass));
},
extractCredentialTypesFromMessage: function(errorMessage) {
/*
* Error message examples:
* - "No credential found for types [SSH Password,SSH Private Key]"
* - "Target is blacklisted. No valid credential found for type [Windows]"
*/
var regexMatches = errorMessage.match(/\[(.*?)\]/);
if (regexMatches.length == 2)
return regexMatches[1].toLowerCase().split(',');
return ['unknown_credential_type'];
},
/*
* Retrieve the OS type for the error
*/
getOsForError: function() {
var os = this.getParameter("sys_class_name");
if (JSUtil.notNil(os))
return os;
var classy = this.getParameter('use_class')+'';
if (JSUtil.nil(classy))
os = null;
switch(classy) {
case 'discovery_classy_unix':
os = 'cmdb_ci_unix_server';
break;
case 'discovery_classy_windows':
os = 'cmdb_ci_win_server';
break;
case 'discovery_classy_snmp':
os = 'cmdb_ci_netgear';
break;
}
return os;
},
/*
* Optionally override this in the sensor to handle warnings.
*/
handleWarning: function(warn) {
DiscoveryLogger.warn(warn, 'Discovery', this.getEccQueueId());
if (g_device)
g_device.issue();
},
isDebugging: function() {
if (this._debug === null)
this._debug = ( this.getParameter('debug') == 'true' );
return this._debug;
},
setDebugging: function(debug) {
this._debug = ( !!debug );
},
debug: function(msg) {
if (this.isDebugging())
gs.log("Sensor debug: " + msg);
},
reclassify: function(ciClass) {
var gr = this.getCmdbRecord();
if (!gr)
return;
var oldClass = gr.sys_class_name;
// staging bump
if (gr.getTableName().startsWith("s_") && !ciClass.startsWith("s_"))
ciClass = "s_" + ciClass;
var cgr = g_disco_functions.reclassify(gr, ciClass, this.getSensorName());
var newClass = cgr.sys_class_name;
if (oldClass != newClass) {
g_device.log("Classified device as " + newClass);
this.cache[cgr.sys_id] = cgr;
}
return cgr;
},
addDiscoveryCiStuff: function(gr, sn) {
var currDateTime = ''+new GlideDateTime();
gr.discovery_source = sn || sourceName;
gr.setValue('last_discovered', currDateTime);
if (!gr.first_discovered)
gr.setValue('first_discovered', currDateTime);
var location = this.getLocationID();
if (location)
gr.location = location;
},
updateObjectSource: function(gr, sn) {
if (JSUtil.notNil(g_status)) {
os = new ObjectSource(sn || sourceName, gr.sys_class_name, gr.sys_id, g_status.source);
os.setValue("last_scan", new GlideDateTime().getDisplayValue());
os.process();
}
},
update: function(data) {
var gr = this.getCmdbRecord();
if (gs.nil(gr))
return;
for (var fieldName in data) {
var value = data[fieldName];
if (JSUtil.notNil(value)) {
// PRB632583: Use setValue() here instead of gr[fieldName] = value
// GlideRecord returns times in UTC and setValue() accepts
// dates in UTC, but assignment uses the current timezone.
gr.setValue(fieldName, value);
}
}
g_disco_functions.updatedHistory(gr, this.getSensorName(), this.getEccQueueId());
this.cache[gr.update(this.getSensorName())] = null; // invalidate the cache after update
// update the necessary related lists
if (this._deviceCi === null)
new DiscoveryReconciler(this.getCmdbRecord(), this.relatedListdataObj).process();
else
this._deviceCi.write();
},
save: function() {
if (!this._defaultSave)
return;
// if we have no DeviceHistory (g_device), then we're in the identity phase of a CI scan, and we don't want to save anything...
if (!g_device && JSUtil.nil(this.getParameter('cmdb_ci')))
return;
this.update((this._deviceCi === null ? current : this._deviceCi.data));
},
// Allow a way to disable the default saving of the current and reconcilers...
setDefaultSave: function(value) {
this._defaultSave = value;
},
logWarningPayload: function(result) {
var warning = this.getAttribute(result, 'warn');
if (warning)
this.processWarning(warning);
},
logDebugInfoPayload: function(result) {
function logMsgArray(arr, sensor) {
try {
var msgs = g_array_util.ensureArray(arr);
for (var i = 0; i < msgs.length; i++) {
var msg = "" + msgs[i];
DiscoveryLogger.debug(msg, 'Discovery', sensor);
}
} catch (e) {
var errMsg = 'Script error in sensor: ' + e;
DiscoveryLogger.warn(errMsg, 'Discovery Sensor', sensor, null);
}
}
if (result && 'true' == gs.getProperty('glide.discovery.log_debug_info', 'false')) {
logMsgArray(result.debug_info, this.getEccQueueId());
logMsgArray(result.log_info, this.getEccQueueId());
}
},
processWarning: function(msg) {
var warningMsg = '' + msg.trim();
// ignore any duplicates...
if (this.warningMap[warningMsg])
return;
// ignore excluded warnings
if (!this.shouldLogItomError(warningMsg))
return;
this.handleWarning(warningMsg);
this.warningMap[warningMsg] = true;
},
checkErrorPayload: function(result) {
var error = this.getAttribute(result, 'error');
if (error) {
error = error + '';
// This is the error for an individual result. There's an overall error for the probe
// which will often have the same value. We saved that error so we can avoid reporting
// the result's error if it's a duplicate.
if (error != this.errorFromResults)
this.processError({msg: error, resultCode: this.getAttribute(result, 'result_code')});
return true;
}
var errors = g_array_util.ensureArray(result.error);
if (errors.length > 0) {
this.processError(errors);
return true;
}
return false;
},
/**
* Returns the attribute with the given name from the result, or null if there was no such attribute. Looks for
* the attribute in three places:
*
* result.@<attr>
* result.results.@<attr>
* result.results.result.@<attr>
*
* The last two places are possibilities in the returned payload of a multiprobe.
*/
getAttribute: function(result, attr) {
var at = result['@' + attr];
if (at)
return '' + at;
if (!result.results)
return null;
at = result.results['@' + attr];
if (at)
return '' + at;
if (!result.results.result)
return null;
at = result.results.result['@' + attr];
if (at)
return '' + at;
return null;
},
processError: function(errors) {
var i;
if (Object.prototype.toString.call(errors) !== '[object Array]')
errors = [errors];
// fill this with trim()'d js strings that aren't dupes of previous errors
var validErrors = [];
for (i = 0; i < errors.length; ++i) {
var error, resultCode;
if (errors[i]) {
if (typeof errors[i] == 'object' && errors[i].msg) {
error = errors[i].msg;
resultCode = errors[i].resultCode;
}
else {
error = errors[i];
resultCode = null;
}
}
//if the error is null, then this is not a valid error, so skip it.
if (JSUtil.nil(error))
continue;
var tError = ''+error.trim();
if (this.errorMap[tError]) // then this is a duplicate. ignore it.
continue;
validErrors.push({msg: tError, resultCode: resultCode});
}
this.handleError(validErrors);
for (i = 0; i < validErrors.length; ++i) // prevent dupes of these errors in future calls
if (errors[i]) {
if (typeof errors[i] == 'object' && errors[i].msg)
this.errorMap[validErrors[i].msg] = true;
else
this.errorMap[validErrors[i]] = true;
}
},
checkEmptyPayload: function(result) {
// Currently if the device history is not created yet. We don't write to it
if (!g_device)
return false;
// tags to check for Windows, SSH and SNMP payloads respectively...
var tags = [result, result.output, result.snmp];
for (var i = 0; i < tags.length; i++)
if (this._checkEmptyPayload(tags[i]))
return true;
return false;
},
_checkEmptyPayload: function(pTag) {
if (!this._checkEmptyPayload1(pTag))
return false;
return true;
},
_checkEmptyPayload1: function(pTag) {
// If empty the tag doesn't exist, then let's not even check it.
if (pTag === undefined)
return false;
// If the tag is null, it means it contains no more data. For example: a tag of <output/> would be null.
if (pTag === null)
return true;
// If the tag is an object, we need to make sure it actually contains data, and not just attributes.
if (pTag instanceof Object)
if (!this._checkContainsData(pTag))
return true;
return false;
},
_checkContainsData: function(obj) {
// If a tag is an object, then we look to see if it actually contains more tags and not just attributes. (The
// attributes have an @ sign in the property, so basically anything that doesn't contain @ means it has more
// tags inside. An example would be <snmp source="10.10.10.10" timeout="true"/>
for (var prop in obj)
if (prop.indexOf("@") == -1)
return true;
return false;
},
logEmptyPayload: function() {
if (g_device)
g_device.log("Sensor has no payload data to process.", this.getSensorName());
},
setCurrentFromJavaMap: function(javaMap) {
var it = javaMap.keySet().iterator();
while (it.hasNext()) {
var key = it.next();
current[key] = javaMap.get(key);
}
},
getAgent: function() {
return g_probe.getAgent();
},
getCmdbCi: function() {
// Some sensors will change the g_device in order
// to use it for certain operations. If this is the case,
// we should make sure to return the manually set value.
if (g_device && g_device.getIsCIChanged())
return g_device.getCmdbCi();
if (JSUtil.notNil(this.getParameter('ci_sys_id')))
return this.getParameter('ci_sys_id');
if (JSUtil.notNil(this.getParameter('cmdb_ci')))
return this.getParameter('cmdb_ci');
if (g_device && g_device.getCmdbCi())
return g_device.getCmdbCi();
this.debug("Unable to find CI record from the device history.");
return null;
},
validSysId: function(sysId) {
return !gs.nil(sysId) && /^[\dA-F]{32}$/i.test(sysId);
},
getCmdbRecord: function() {
var ci = this.getCmdbCi();
if (!this.validSysId(ci))
return null;
var gr;
if (this.cache[ci])
gr = this.cache[ci];
else {
gr = new GlideRecord('cmdb_ci');
if (!gr.get(ci)) {
this.debug("Unable to retrieve CI record from the database.");
return null;
}
var gru = GlideScriptRecordUtil.get(gr);
gr = gru.getRealRecord();
this.cache[ci] = gr;
}
return gr;
},
/**
* Retrieve the ci sys_id of the current device, return null if there isn't any.
*/
getCiSysID: function() {
if (JSUtil.nil(g_device))
return "";
return JSUtil.notNil(g_device.getSourceCmdbCi()) ? g_device.getSourceCmdbCi() : this.getParameter('cmdb_ci');
},
/**
* Retrieves a token that can be used to uniquely identify this discovery
* run among others. Currently, the token is the SysID of the Discovery Status
* table / class (provided by g_status).
*/
getAgentCorrelator: function() {
return '' + this.getParameter("agent_correlator");
},
// Return the discovery source.
getDiscoverySource: function() {
if(typeof this.discovery_source != 'undefined' && StringUtil.notNil(this.discovery_source))
return this.discovery_source;
else {
var statusId = this.getAgentCorrelator();
var gr = new GlideRecord("discovery_status");
gr.get(statusId);
this.discovery_source = gr.source;
return this.discovery_source;
}
},
getEccQueueId: function() {
return ''+g_probe.getEccQueueId();
},
getEccQueueRecord: function() {
return g_probe.getEccQueueRecord();
},
getParameter: function(name) {
return g_probe.getParameter(name);
},
getSensorName: function() {
return (typeof sensorName == 'undefined') ? "": sensorName; //return empty string if sensorName is not defined
},
getSource: function() {
return (typeof source == 'undefined') ? "": source; //return empty string if source (IP) is not defined;
},
getLogger: function() {
var dh = this.getDeviceHistory();
var dgr = dh.getDeviceRecord();
var dhid;
if (JSUtil.notNil(dgr))
dhid = dgr.sys_id;
var dl = new DiscoveryLogger(this.getAgentCorrelator(), dhid);
dl.setSensor(this.getEccQueueId());
dl.setSource(this.getSensorName());
return dl;
},
addToRelatedListWithDeleteGr: function(tableName, dataArray, refName, keyName, deleteGr) {
this.relatedListdataObj[tableName] = {
data: dataArray,
refName: refName,
keyName: keyName,
deleteGr: deleteGr
};
},
addToRelatedList: function(tableName, dataArray, refName, keyName) {
this.addToRelatedListWithDeleteGr(tableName, dataArray, refName, keyName, false);
},
updateCounts: function() {
this.updateDeviceCount();
},
updateDeviceCount: function() {
this.getDeviceHistory().completed();
},
setTriggerProbes: function(value) {
this.ASensor.setTriggerProbes(value);
},
warn: function(message) {
DiscoveryLogger.warn(message, this.getSensorName(), this.getEccQueueId());
},
isAnotherClassificationActive: function() {
var source = this.getSource();
var status = this.getStatus();
var classificationMutex = this.getClassificationMutex(source, status);
try {
var gr = new GlideRecord('discovery_device_history');
gr.addQuery('source', this.getSource());
gr.addQuery('status', this.getAgentCorrelator());
gr.addQuery('classification_probe', '!=', probe);
var qc = gr.addQuery('current_state', 'Classifying');
qc.addOrCondition('current_state', 'Identifying');
qc.addOrCondition('last_state', 'Updated CI');
gr.query();
return gr.hasNext();
} finally {
classificationMutex.release();
}
},
/**
* @param undefined|boolean skipFailState Do not update the g_device state if no further classifiers could be launched (keeping previous error).
*/
fireAdditionalClassifier: function(skipFailState) {
var cp = new ClassifierProbes(this.getParameter('classifiers'), this.getParameter('deviceHistoryParams'));
if (!this.isAnotherClassificationActive()) {
// If we are done with host classification - lets trigger credential less discovery
if (cp.hostClassificationDone())
this.launchCredentiallessDeviceDiscovery(this.getSource());
else
cp.launch();
}
if (g_device) {
var status = new DiscoveryStatus(g_device.getStatus());
if (skipFailState !== true) {
g_device.state(DiscoverySensor.HistoryStates.ACTIVE_COULDNT_CLASSIFY, '', status.logStateChanges, 'Discovery', this.getEccQueueId());
this.errorManager.addError(new SNC.DiscoveryErrorMsg('SN-1582', g_device.source, status.sysID, null, "Host is active, but unable to classify", null));
}
}
},
/**
* Launches supplementary classifications after a higher-priority identification succeeds, once again in order of priority.
*/
fireSupplementaryClassifiers: function() {
// Check whether classifier probe functionality is installed
if (typeof ClassifierProbes === 'undefined')
return;
var classifierProbes = new ClassifierProbes(this.getParameter('classifiers'), this.getParameter('deviceHistoryParams'));
classifierProbes.launchSupplementary();
},
/*
* Expose the base values in CIData to the classifiers. Currently used by Windows, Unix and SNMP.
*/
addCIDataBaseValue: function(obj) {
var ciData = new CIData();
ciData.fromXML(this.getParameter('cidata'));
var ci_data = this.ciData.getData();
for (var prop in ci_data)
obj[("cidata."+prop)] = ci_data[prop];
},
/*
* This is used to add variables to the ci_data variable after running classifications.
* It basically runs the classification script and store the variables in ci_data.
*/
runClassificationScript: function(dClasser, ciData) {
var setFields = dClasser.runScript();
var it = setFields.keySet().iterator();
while (it.hasNext()) {
var key = it.next();
ciData[key] = setFields.get(key);
}
},
/**
* Retrieves the location of the current discovery.
* @returns string|null The SysID of the Location for the current sensor or NULL if not found.
*/
getLocationID: function() {
if (typeof(this._locationID) !== 'undefined') // initialized on demand by this method.
return this._locationID;
this._locationID = null; // initialize
var scheduleGr = new GlideRecord('discovery_schedule');
if (!scheduleGr.get(g_status.scheduleID))
return null;
if (!scheduleGr.location)
return null;
this._locationID = ''+scheduleGr.location.sys_id;
return this._locationID;
},
/**
* Helper method to remove all nil properties from current.
*/
removeNilProps: function() {
for (var fieldName in current) {
if (JSUtil.nil(current[fieldName]))
delete current[fieldName];
}
},
/**
* Helper method to trigger a DNS probe for the given IP addresses.
*/
triggerDnsProbe: function(probeName, ipAddresses, source) {
if (gs.nil(ipAddresses) || ipAddresses.length == 0)
return;
// join and pass the ip_addresses as a probe parameter
var probe = new Probe.get(probeName);
if (probe) {
probe.setSource(source || this.getSource());
probe.addParameter('ip_addresses', ipAddresses.join());
probe.create(this.getAgent(), this.getEccQueueId());
}
},
_recordStopWatch: function(name, time) {
var logSensorTime = JSUtil.toBoolean(gs.getProperty("glide.discovery.log_sensor_metrics", "false"));
if (!logSensorTime)
return;
// If we're just running sensor test, no need to log the metrics
if ((typeof g_mockSensor != 'undefined') && g_mockSensor == true)
return;
var gr = new GlideRecord("discovery_metric");
gr.discovery_status = this.getAgentCorrelator();
gr.ip_address = this.getSource();
gr.context = this.getSensorName();
gr.identifier = name;
gr.process_time = time;
gr.insert();
},
classifierIsDisabled: function(classifier) {
var isDisabled = classifier.getValue("disabled");
if (JSUtil.nil(isDisabled)) // if value has never been set, continue normally since default is false
return false;
return JSUtil.toBoolean(parseInt(isDisabled));
},
handleDisabledClassifier: function(classifier, source) {
var disabledClassifierMsg = gs.getMessage("Classifier {0} has been disabled, stopping discovery", classifier.getValue("name"));
DiscoveryLogger.info(disabledClassifierMsg, source, this.getEccQueueId());
// update device history state, if we have one...
if (g_device) {
var status = new DiscoveryStatus(g_device.getStatus());
g_device.state(DiscoverySensor.HistoryStates.CLASSIFIED_BUT_DISABLED, '', status.logStateChanges, source, this.getEccQueueId());
}
},
triggerProbe: function(probeName, parameters) {
var name,
probe = new Probe.get(probeName, {"ci_sys_id": this.getCmdbCi()});
if (probe) {
probe.setSource(this.getSource());
probe.addParameter("cmdb_ci", this.getCmdbCi());
probe.setEccPriority(this.getParameter("priority"));
probe.setMidSelectDetails(this.getParameter('mid_selector_details'));
probe.addParameter('agent_correlator', this.getAgentCorrelator());
for (name in parameters)
probe.addParameter(name, parameters[name]);
probe.create(this.getAgent(), this.getEccQueueId());
}
},
// Trigger any conditional probes for a sensor
// prepFn {function} Function to prepare parameters for the probe. It's possible to trigger
// the same probe more than once. prepFn will be called repeatedly until
// it returns false. The value returned from prepFn will be passed to the
// discovery_sensor_probe_conditional script (which can also return false
// to keep the probe from being triggered.)
// parms {Object} Parameters provided by the sensor. A copy of this object is passed to prepFn.
// copiedParms {Array} Array of parameters to copy from the probe that caused this sensor to run.
triggerProbes: function(prepFn, parms, copiedParms) {
var probeParms, data, name,
probe = new GlideRecord('discovery_sensor_probe_conditional');
// Triggering probes will re-trigger the conditional probes (without evaluating
// the condition). Let's just not trigger non-conditional probes for now.
this.setTriggerProbes(false);
probe.addQuery('active', 'true');
probe.addQuery('parent', sensorId);
probe.query();
var first = true;
prepFn = prepFn || function() {
if (first) {
first = false;
return true;
};
}
// Walk over each probe that might get triggered, get parameters for the probe
// and trigger it
while (probe.next()) {
first = true;
// (1) Get both the parameters copied from the previous probe and any parameters provided by the sensor
// (2) call prepFn() with these parameters and the discovery_sensor_probe_conditional GlideRecord
// (3) Loop until prepFn returns falsey
while ((probeParms = getParms()) && (data = prepFn(probeParms, probe))) {
// If there's no condition script just copy all values provided by the sensor
if (!probe.condition_script && typeof data == 'object') {
for (name in data)
probeParms[name] = data[name];
}
// Make sure all the parameters ara JavaScript strings
for (name in probeParms) {
if (probeParms[name] instanceof Packages.java.lang.String)
probeParms[name] = '' + probeParms[name];
else if (typeof probeParms[name] != 'string')
probeParms[name] = JSON.stringify(probeParms[name]);
}
// Trigger the probe if there's no condition script or if the condition script returns truthy
if (!probe.condition_script || eval('(' + probe.condition_script + ')(probeParms, data)'))
this.triggerProbe(probe.child.name, probeParms);
}
}
function getParms() {
var name,
p = { };
// Copy the sensor's parameters
if (parms) {
for (name in parms)
p[name] = '' + parms[name];
}
// Now copy all requested parameters from the previous probe
copiedParms && copiedParms.forEach(copyParm);
return p;
function copyParm(name) {
if (g_probe.getParameter(name))
p[name] = '' + g_probe.getParameter(name);
}
}
},
/*
* Insert errors into automation_error_msg table.
* @param errorMap: A JSON array that contains multiple JSON objects that has attributes "error_code" and "error_message"
* e.g [{"errorCode": "SN-5000", "errorMessage": "something is wrong"}, {"errorCode": "SN-5001", "errorMessage": "something is wrong}]
*/
handleErrorMap: function(errorMap) {
var ciSysId = this.getCiSysID();
var statusSysId = this.getAgentCorrelator();
var source = this.getSource();
for (var i in errorMap)
this.errorManager.addError(new SNC.DiscoveryErrorMsg(errorMap[i].errorCode, source, statusSysId, ciSysId, errorMap[i].errorMsg, this.getParameter("sys_class_name")));
},
/**
* Conditionally launches Credentialless Device Discovery Pattern
*/
launchCredentiallessDeviceDiscovery: function(sourceIP) {
// if credentialless is launched from shazzam, deviceHistoryParams -> Object
// otherwise deviceHistoryParams -> String
var deviceHistoryParams = {
ecc_queue : this.getEccQueueId()
};
this.deviceHistoryParams = !this.deviceHistoryParams ? JSON.stringify(deviceHistoryParams) : JSON.stringify(this.deviceHistoryParams);
// PRB1383537 - Sending cidata along
var probeParams = {
cidata: this.ciData.toXML()
};
// Credentialless Device Discovery classification enabled?
// Set default property value to disabled (false).
if (gs.getProperty("mid.discovery.credentialless.enable", "false") == "true")
new SncCredentiallessDeviceDiscovery().launch(this.getAgentCorrelator(), this.getParameter('mid_selector_details'), this.getEccQueueId(), sourceIP, probeParams, this.deviceHistoryParams);
},
type: "DiscoverySensor"
};
Sys ID
778011130a0a0b2500c4595ad1d1d768