Name

global.DiscoveryCMDBUtil

Description

This class contains useful utility functions for interacting with the CMDB API for identification and reconciliation.

Script

// Discovery
  
/***
* The purpose of this class is to provide an adapter API between the Identity and
* Process Classification phases of Discovery and the CMDB Identification Engine API
*/

ArrayPolyfill;
var DiscoveryCMDBUtil;
(function() {

  DiscoveryCMDBUtil = {
      /***
       * useCMDBIdentifiers()
       *
       * @return boolean true, if Discovery should use the CMDB ID Engine, false if should use legacy engine
       */
      useCMDBIdentifiers: useCMDBIdentifiers,

      /***
       * checkInsertOrUpdate(ciData, logger)
       *
       * Used to call the CMDB ID Engine to check whether the given ciData would result in an
       * insert or update without actually committing the data to the CMDB
       *
       * @param CIData, ciData (required) The CIData obj to use for identification
       * @param DiscoveryLogger, logger (required) The DiscoveryLogger instance to use for errors
       *
       * @return {
       *              success:    boolean, true if identification was sucessful, false if unsuccessful
       *              insert:     boolean, true if operation would be an insert (false if would be update)
       *              sysId:      string, sys_id of matching record if update, null if no match (insert)
       *              className:  string, sys_class_name of matching record, null if no match (insert)
       *              attempts:   [], list of identification attempts during matching process
       *         }
       */
      checkInsertOrUpdate: checkInsertOrUpdate,

      /***
       * insertOrUpdate(ciData, logger)
       *
       * Used to call the CMDB ID Engine for identification with the given ciData and commit the insert or update
       *
       * @param CIData, ciData (required) The CIData obj to use for identification
       * @param DiscoveryLogger, logger (required) The DiscoveryLogger instance to use for errors
       *
       * @return {
       *              success:    boolean, true if identification was sucessful, false if unsuccessful
       *              insert:     boolean, true if operation would be an insert (false if would be update)
       *              sysId:      string, sys_id of matching record if update, null if no match (insert)
       *              className:  string, sys_class_name of matching record, null if no match (insert)
       *              attempts:   [], list of identification attempts during matching process
       *         }
       */
      insertOrUpdate: insertOrUpdate,

      /***
       * rerunIDWithLogContext(contextID, ieDebugLevel, serviceContextDebugLevel, logger)
       * 
       * Used to call CMDB ID Engine for rerun identification context, in order to provide more debug logging.
       */
      rerunIDWithLogContext: rerunIDWithLogContext,

      /***
       * createOrUpdateApp(appGr, hostSysId, relType, attrMap)
       *
       * Used to call the CMDB ID Engine to create or update applications
       *
       * @param GlideRecord appGr (required) The application GlideRecord to update or create
       * @param string hostSysId (required) The sys_id of the host to tie the application to
       * @param string relType (optional; default: Runs on::Runs) The relationship type between the app and host
       * @param {} attrMap (optional) A map of attributes and their values to update for the app
       *
       * @return string sys_id of successful insert/update or null/Unknown if failure
       */
      createOrUpdateApp: createOrUpdateApp,

      /***
       * logIDAttempts(attempts, logger)
       *
       * Used to log match attempts by the CMDB ID Engine
       *
       * @param [], atempts (required) The list of identification attempts to log
       * @param DiscoveryLogger, logger (required) The DiscoveryLogger instance to use for logging
       */

      logIDAttempts: logIDAttempts,
      type: 'DiscoveryCMDBUtil'
  };

  var json = new JSON();
  var cmdbApi = SNC.IdentificationEngineScriptableApi;
  var lookupTableCache = {};

  function useCMDBIdentifiers() {
      return (JSUtil.toBoolean(gs.getProperty("glide.discovery.use_cmdb_identifiers", "false")) || GlidePluginManager.isRegistered('com.snc.service-mapping'));
  }

  function createOrUpdateApp(appGr, hostSysId, relType, attrMap) {
      if (JSUtil.nil(appGr) || JSUtil.nil(hostSysId))
          return null;

      relType = relType || "Runs on::Runs";

      var hostGr = new GlideRecord("cmdb_ci");
      if (!hostGr.get(hostSysId))
          return null;

      var hostClass = hostGr.sys_class_name + '';

      /***
       * Convert the app (GlideRecord) to a normal js object and update attributes if appropriate
       * This ensures that we use the IE because changing values in the passed down GlideRecord
       * Can possibly circumvent IE updates due to how normal Discovery Sensor processing works
       */
      var appClass = appGr.getTableName();
      var appValues = glideRecordToJS(appGr, attrMap);

      /***
       * Remove the sys_id so we utilize IE matching instead of assuming we have the correct application after ADM
       */
      delete appValues.sys_id;

      var payload = {
          "items": [{
                  "className": appClass,
                  "values": appValues
              }, // App
              {
                  "className": hostClass,
                  "values": {
                      "sys_id": hostSysId
                  }
              } // Host
          ],

          "relations": [{
              "parent": 0,
              "child": 1,
              "type": relType
          }]
      };

      var discoverySource = appValues.discovery_source || gs.getProperty('glide.discovery.source_name', "ServiceNow");
      var result = json.decode(cmdbApi.createOrUpdateCI(discoverySource, json.encode(payload)));
      return getAppSysIdFromResult(result, appClass);

      /***
       * Returns the sys id of the given IE result
       * Assumes that there is only one app per payload
       */
      function getAppSysIdFromResult(result, appClass) {
          if (JSUtil.nil(result) || JSUtil.nil(result.items) || JSUtil.nil(appClass))
              return null;

          var sysId = null;
          for (var i = 0; i < result.items.length; i++) {
              var item = result.items[i];
              if (item.className === appClass) {
                  sysId = item.sysId;
                  break;
              }
          }

          return sysId;
      }

      /***
       * Converts a GlideRecord object into a plain js object (ignoring null attributes)
       * And merge with optional map of attributes to update
       */
      function glideRecordToJS(gr, attrMap) {
          var obj = attrMap || {};

          for (var grField in gr) {
              if (obj[grField])
                  continue;

              var value = gr.getValue(grField);
              if (JSUtil.nil(value))
                  continue;

              obj[grField] = value;
          }

          return obj;
      }
  }

  function generateIDPayload(ciData) {
      var data = ciData.getData();
      var className = data.sys_class_name;
      var payload = {
          "items": [{
              "className": className,
              "values": valuesToJson(data),
              "lookup": lookupValuesToJson(ciData, className)
          }]
      };

      return json.encode(payload);

      /***
       * Make sure that we coerce java strings to javascript
       */
      function valuesToJson(obj) {
          var values = {};
          for (var fieldName in obj)
              values[fieldName] = obj[fieldName] + '';

          return values;
      }

      function itemsToJson(objs, table) {
          var items = [];

          objs.forEach(function(o) {
              if (JSUtil.nil(o))
                  return;

              items.push({
                  className: table,
                  values: valuesToJson(o)
              });
          });

          return items;
      }

      /*** 
       * Currently we rely on lookups on cmdb_serial_number and cmdb_ci_network_adapter
       * tables for device identification, so we need to build the lookup payload accordingly
       */
      function lookupValuesToJson(ciData, className) {
          var lookups = [];

          if (isActiveLookupRule(className, "cmdb_serial_number"))
              lookups = lookups.concat(processSerialNumbers(ciData));

          if (isActiveLookupRule(className, "cmdb_ci_network_adapter"))
              lookups = lookups.concat(processNetworkAdapters(ciData));

          return lookups;

          /***
           * Checks if the given lookup table has an active corresponding lookup rule entry for the given class
           */
          function isActiveLookupRule(className, lookupTable) {
              if (!lookupTableCache[className]) {
                  lookupTableCache[className] = {};
                  j2js(cmdbApi.getLookupRuleTablesForClass(className)).forEach(function(lookupTable) {
                      lookupTableCache[className][lookupTable] = true;
                  });
              }

              return lookupTableCache[className][lookupTable] || false;
          }

          /***
           * Serial numbers must be checked for validity before used for lookup
           * We only want to send valid serial numbers for the lookup & CI reconciliation
           * But we still want the intact list of serial numbers for cleaning up absent/invalid
           */
          function processSerialNumbers(ciData) {
              var validSNs = [];
              var serialNumbers = ciData.getRelatedList("cmdb_serial_number", "cmdb_ci");
              for (var i = 0; i < serialNumbers.length; i++) {
                  var srl = serialNumbers[i];
                  var sn = new SncSerialNumber();
                  if (sn.isValid(srl.serial_number))
                      validSNs.push(srl);
              }

              return itemsToJson(validSNs, "cmdb_serial_number");
          }

          /***
           * Network adapters must be checked for validity before used for lookup
           */
          function processNetworkAdapters(ciData) {
              var validAdapters = [];
              var networkAdapters = ciData.getRelatedList("cmdb_ci_network_adapter", "cmdb_ci");
              for (var i = 0; i < networkAdapters.length; i++) {
                  var adapter = networkAdapters[i];
                  if (isInvalidAdapter(adapter))
                      continue;

                  var cleanedAdapter = {};
                  for (var fieldName in adapter) {
                      if (fieldName === "mac_address") {
                          var ma = SncMACAddress.getMACAddressInstance(adapter.mac_address);
                          cleanedAdapter.mac_address = '' + ma.getAddressAsString();
                      } else {
                          var value = adapter[fieldName];
                          // We only care about flat values for the NIC for use in identifiers,
                          // the related list comes with arrays of IP addresses and route maps
                          // that shouldn't be passed to the Identification Engine
                          if (value instanceof Array || typeof value === 'object')
                              continue;

                          cleanedAdapter[fieldName] = value + '';
                      }
                  }

                  validAdapters.push(cleanedAdapter);
              }

              return itemsToJson(validAdapters, "cmdb_ci_network_adapter");

              /*** 
               * A valid NIC must have a mac address, ip address, name and cannot be a localhost or loopback adapter
               */
              function isInvalidAdapter(adapter) {
                  if (JSUtil.nil(adapter.mac_address) || JSUtil.nil(adapter.ip_address) || JSUtil.nil(adapter.name))
                      return true;

                  // Localhost adapter check
                  if (adapter.ip_address == '127.0.0.1')
                      return true;

                  if (adapter.ip_addresses) {
                      for (var i = 0; i < adapter.ip_addresses.length; i++) {
                          if (adapter.ip_addresses[i].ip_address == '127.0.0.1')
                              return true;
                      }
                  }
                  var loopback_name = adapter.name === 'lo' || adapter.name.match(/loopback/i);
                  var filter_loopback = !JSUtil.toBoolean(gs.getProperty('glide.discovery.allow_loopback_adapters', false));
                  return JSUtil.nil(adapter.name) || adapter.name.match(/localhost/i) || (loopback_name && filter_loopback);
              }
          }
      }
  }

  function logIDAttempts(attempts, logger) {
      if (JSUtil.nil(attempts))
          return;

      var resultMap = {
          "MATCHED": "Match",
          "NO_MATCH": "No Match",
          "MULTI_MATCH": "Multi-Match Error",
          "SKIPPED": "Skipped Identifier Entry"
      };

      attempts.forEach(function(attempt, i) {
          var identifier = attempt.identifierName !== null ? attempt.identifierName : "";
          var result = resultMap[attempt.attemptResult];
          var table = attempt.searchOnTable !== null ? attempt.searchOnTable : "";
          var attributes = attempt.attributes !== null ? attempt.attributes.join(", ") : "";
          var hybridAttributes = attempt.hybridEntryCiAttributes != null ? attempt.hybridEntryCiAttributes.join(", ") : "";
          var hybridLogMsg = hybridAttributes ? " with hybrid attributes: " + hybridAttributes : "";
          var logMsg = "Rule " + (i + 1) + ": Searched on <" + table + "> for attributes: " + attributes + hybridLogMsg + ": " + result;

          if (result === resultMap.MULTI_MATCH)
              logger.warn(logMsg, "Identifier: " + identifier);
          else
              logger.info(logMsg, "Identifier: " + identifier);
      });
  }

  function parseIDResult(result, logger, sysClassName) {
      var idResultObj = {
          success: false,
          insert: false,
          sysId: null,
          className: null,
          logContextId: null,
          attempts: [],
          errors: []
      };

      // Set the log context ID if there is one
      idResultObj.logContextId = result.logContextId;

      if (JSUtil.nil(result) || JSUtil.nil(result.items) || !result.items.length || result.items.length > 1)
          return idResultObj;

      // There should be only one item returned
      var item = result.items[0];

      if (item.errors) {
          if (logger) {
              // Log attempts in case of multi-match error to see which failed
              logIDAttempts(item.identificationAttempts, logger);
              item.errors.forEach(function(error) {
                  // These are redundant to log since any error will cause an 'ABANDONED' msg
                  // and we already specifically logged the multi-match attempt
                  if (error.error !== "MULTI_MATCH" && error.error !== "ABANDONED") {
                      idResultObj.errors.push(error.error);
                      logger.error("CMDB Identification Error: " + error.message);
                  }
              });

              // to prevent double logging, log only when there is a logger
              logITOMError(item.errors, sysClassName);
          }

          return idResultObj;
      }

      idResultObj.success = true;
      idResultObj.insert = item.operation === "INSERT" ? true : false;
      idResultObj.sysId = item.sysId || null; // will be undefined if its a check call which returns insert
      idResultObj.className = item.className;
      idResultObj.attempts = item.identificationAttempts;

      return idResultObj;
  }

  function checkInsertOrUpdate(ciData, logger) {
      var payload = generateIDPayload(ciData);
      var result = json.decode(cmdbApi.identifyCI(payload));
      return parseIDResult(result, logger, ciData.getData().sys_class_name);
  }

  function insertOrUpdate(ciData, logger) {
      var payload = generateIDPayload(ciData);

      // Before we insert or update a CI - lets see if we need to reconcile with a CI created by credential less discovery created CI
      new SncCredentiallessDeviceDiscovery().reconcileJson(payload);

      var result = json.decode(cmdbApi.createOrUpdateCI(gs.getProperty('glide.discovery.source_name', "ServiceNow"), payload));
      return parseIDResult(result, logger, ciData.getData().sys_class_name);
  }

  // Possible value of ieDebugLevel and serviceCacheDebugLevel: Info, Warn, Error, Debug, DebugVerbose, DebugObnoxious
  function rerunIDWithLogContext(logContextId, ieDebugLevel, serviceCacheDebugLevel, logger) {
      var runId = cmdbApi.runIdentificationContext(logContextId, ieDebugLevel, serviceCacheDebugLevel);
      var gr = new GlideRecord('cmdb_ie_run');
      if (runId)
          gr.get('sys_id', runId);
      var result = json.decode(gr.getValue('output_payload'));
      result.logContextId = logContextId;
      return parseIDResult(result, logger);
  }

  function logITOMError(errors, sysClassName) {
      if (JSUtil.nil(errors))
          return;

      var errorManager = new SNC.DiscoveryErrorManager();

      // full list of codes in IdentificationError.java
      for (var i = 0; i < errors.length; i++) {
          var error = errors[i];
          var errorKey = error.error + '';

          if (errorKey == 'ABANDONED') // This has a message of 'Too many other errors'.  Not very useful.
              continue;

          var code = 'SN-5999'; // Unexpected error
          if (errorKey == 'MULTIPLE_DUPLICATE_RECORDS')
              code = 'SN-1561';
          else if (errorKey == 'INVALID_INPUT_DATA')
              code = 'SN-1552';

          errorManager.addError(new SNC.DiscoveryErrorMsg(code, g_device.source, g_device.status, null, error.message, sysClassName));
      }
  }

})();

Sys ID

06a8b503c3b73100d8d4bea192d3ae2d

Offical Documentation

Official Docs: