Name

global.AbstractDeleteStrategy

Description

No description available

Script

var AbstractDeleteStrategy = Class.create();

AbstractDeleteStrategy.LOGGER_NAME = 'DeleteStrategy';
AbstractDeleteStrategy.LOGGER_NAME_LOG_PREFIX = '[' + AbstractDeleteStrategy.LOGGER_NAME + ']';

AbstractDeleteStrategy.prototype = {
  discoverySource: 'ServiceNow',
  deleteStrategies: {
  	// 1 is "keep" but we don't use it in delete strategy
  	2: {
  		description: 'delete CI'
  	},
  	3: {
  		description: 'mark as absent'
  	},
  	4: {
  		description: 'delete relations'
  	},
  	5: {
  		description: 'mark as retired'
  	}
  },

  initialize: function() {
  	// Can't initialize at the top. Functions are undefined on that phase
  	this._intializeDeleteStategyToDeleteFunctionMapping();
  },

  _intializeDeleteStategyToDeleteFunctionMapping: function() {
  	this.deleteStrategies[2].deletionFunction = this._deleteCI.bind(this);
  	this.deleteStrategies[3].deletionFunction = this._markAbsent.bind(this);
  	this.deleteStrategies[4].deletionFunction = this._deleteRelations.bind(this);
  	this.deleteStrategies[5].deletionFunction = this._markRetired.bind(this);
  },

  /*
   *_getRelationshipPaths: function() {
   *  // a sublass can provide additional relationship paths to walk down ex:
   *
   *  var additionalRelationPaths = {};
   *
   *  additionalRelationPaths['cmdb_ci_endpoint_block'] = [
   *     'cmdb_ci_endpoint_block', 'cmdb_ci_storage_volume', 'cmdb_ci_aws_datacenter'
   *  ];
   *  
   *  return additionalRelationPaths;
   * },
   */


  /*
  	scheduleStartTime - GlideDate
  	ciClass - class
  	patternInputs - specific discovery content
  	patternContext - specific discovery content
  	ciTypeStrategyMapping - map between class and delete strategy for children CIs
  	agentCorrelator - Discovery Status - optional for logging
  */
  reconcile: function(scheduleStartTime, ciClass, patternInputs, patternContext, ciTypeStrategyMapping, agentCorrelator) {
  	/*
  	 * The reason creating this variable is to reduce the number of arguments on future function calls
  	 * We can't have it as "this.varibaleName" since the value is shared among multiple instances
  	 * of this object
  	 * At the moment, I [Oron] have decided to create a single object from input parameters 
  	 * to pass through, rather having functions with ~10+ arguments
  	 * The algorithm did NOT change
  	 */
  	var deleteStrategyData = {
  		inputParams: {
  			scheduleStartTime: scheduleStartTime,
  			targetCiClass: ciClass,
  			patternInputs: patternInputs,
  			patternContext: patternContext,
  			ciTypeToStrategy: ciTypeStrategyMapping,
  			statusSysId: agentCorrelator
  		},
  		maxCIsToDelete: GlideProperties.get('glide.discovery.delete_strategy.max_deletion_records', 2000),
  		maxCIsInSearchGroup: GlideProperties.get('glide.discovery.delete_strategy.max_ci_search_group', 100),
  		maxRelatedEntriesToDeleteInIteration: GlideProperties.get('glide.discovery.delete_strategy.max_related_entried_delete_in_iteration', 500),
  		cisDeleted: 0,
  		relatedEntries: this._findRelatedEntries(ciClass),
  		nodeLogger: new sn_automation.AutomationAPI().getNodeLogger(AbstractDeleteStrategy.LOGGER_NAME)
  	};

  	deleteStrategyData.nodeLogger.info(this._buildLogMessage('Starting delete strategy'));
  	deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Input parameters:\n' + JSON.stringify(deleteStrategyData.inputParams)));
  	deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Configuration:\nMax CIs to handle:' + 
  		deleteStrategyData.maxCIsToDelete + '\nCIs group size for related entries deletion: ' + 
  		deleteStrategyData.maxCIsInSearchGroup + '\nRelated entries config: ' + JSON.stringify(deleteStrategyData.relatedEntries)));

  	if (this._getCIs) {
  		deleteStrategyData.nodeLogger.info(this._buildLogMessage('"_getCIs" function was implemented in provided script. Getting CIs to delete'));
  		var reconcilables = this._getCIs(scheduleStartTime, ciClass, patternInputs, patternContext, deleteStrategyData.nodeLogger);
  		
  		if (reconcilables) {
  			var message = 'Reconcilable CIs were specified by the subclass. Processing delete strategy for ' + ciClass;
  			deleteStrategyData.nodeLogger.info(this._buildLogMessage(message));
  			deleteStrategyData.nodeLogger.debug(this._buildLogMessage(' CIs to delete: ' + JSON.stringify(reconcilables)));

  			for (var item in reconcilables) {
  				deleteStrategyData.nodeLogger.debug(this._buildLogMessage(' Deleting ' + reconcilables[item]));
  				this._reconcileOnStrategy(deleteStrategyData, reconcilables[item]);
  			}
  			
  			deleteStrategyData.nodeLogger.debug(this._buildLogMessage(' Deleted total of ' + reconcilables.length + ' CIs'));
  		}
  		return;
  	}
  		
  	// a subclass MUST provide an implementation of _getBoundaryConditions to find the CI's to reconcile
  	// if no boundaries are specified, stop processing.. without a boundary condition, we _MIGHT_ end up
  	// marking all the CI's in the CMDB as stale which is not a desirable behavior
  	deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Getting boundaries for context:\n' + JSON.stringify(patternContext)));
  	var boundaries = this._getBoundaryConditions(patternContext, deleteStrategyData.nodeLogger);
  	
  	if (boundaries && boundaries.length == 0) {
  		var message = 'No boundary conditions were provided. Cannot process delete strategy without a valid boundary condition';
  		deleteStrategyData.nodeLogger.warn(this._buildLogMessage(message));
  		gs.warn(message, 'DeleteStrategy');
  		return;
  	}
  	var existingRelationPaths = this._getRelationshipPaths();
  	deleteStrategyData.nodeLogger.info(this._buildLogMessage('Processing boundary conditions : ' + boundaries.join()));

  	for (var boundary in boundaries) {
  		var boundaryItem = boundaries[boundary];

  		if (typeof boundaryItem == 'string')
  			boundaryItem = this._getRecordByID(boundaryItem);
  		var boundaryClass = boundaryItem.sys_class_name;

  		deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Processing boundary "' + boundaryItem.getValue('sys_id') + '" of class "' + boundaryClass + '"'));

  		// Get path between our class and ldc
  		var path = [];
  		if (existingRelationPaths && existingRelationPaths[ciClass])
  			path = existingRelationPaths[ciClass];
  		else
  			path = this._getPathBetweenClasses(boundaryClass, ciClass);
  		deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Classpath found between "' + ciClass + '" to "' + boundaryClass + '": ' + path.join('->')));

  		if (path == null) {
  			var message = 'No path could be created between ' + boundaryClass + ' and ' + ciClass;
  			deleteStrategyData.nodeLogger.warn(this._buildLogMessage(message));
  			gs.warn(message, 'DeleteStrategy');
  			continue;
  		}
  		deleteStrategyData.pathToBoundaryClass = path;

  		var levelInPath = path.length - 2;
  		var layerToExplore = {
  			level: levelInPath,
  			className: path[levelInPath],
  			parentCi: boundaryItem.sys_id + ''
  		};

  		deleteStrategyData.nodeLogger.info(this._buildLogMessage('Starting CMDB search for stale CIs'));
  		this._recursiveReconcileOnStrategy(deleteStrategyData, layerToExplore);
  	}
  },

  _recursiveReconcileOnStrategy: function(deleteStrategyData, layerToExplore) {
  	deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Layer to explore: ' + JSON.stringify(layerToExplore)));

  	if (this._internalWalkStep(deleteStrategyData, 'parent', 'child', layerToExplore))
  		return;

  	this._internalWalkStep(deleteStrategyData, 'child', 'parent', layerToExplore);
  },
  
  _internalWalkStep: function(deleteStrategyData, relationDirectionFrom, relationDirectionTo, layerToExplore) {
  	var isDeletionLayer = layerToExplore.className == deleteStrategyData.inputParams.targetCiClass;
  	
  	deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Preparing a query on relations table for ' + relationDirectionTo + ' CIs of type ' + layerToExplore.className + ' that have a relation to ' + layerToExplore.parentCi));
  	
  	var children = this._prepareChildrenQuery(relationDirectionFrom, layerToExplore.parentCi, relationDirectionTo, layerToExplore.className);
  	
  	if (isDeletionLayer) {
  		deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Deletion layer have been reached. Adding additional conditions to the search'));

  		this._logCommonQueryConditions(deleteStrategyData.nodeLogger, relationDirectionTo);
  		this._addDeleteLayerCommonConditions(children, relationDirectionTo);
  		
  		deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Adding condition: ' + relationDirectionTo + '\'s last_discovered is at or before ' + scheduleStartTime));
  		this._addStaleConditions(children, relationDirectionTo, deleteStrategyData.inputParams.scheduleStartTime);
  		
  		this._enhanceDeleteLayerQuery(children, relationDirectionTo, deleteStrategyData.inputParams.scheduleStartTime);
  		
  		message = 'About to find and delete ' + layerToExplore.className + ' records. Deletion strategy is "' + this._getStrategyNameByCode(this._getDeleteStrategyCodeFromData(deleteStrategyData)) + '"';
  		deleteStrategyData.nodeLogger.info(this._buildLogMessage(message));
  	}
  	
  	children.query();
  	var hasChildren = children.hasNext();
  	
  	deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Found ' + children.getRowCount() + ' ' + layerToExplore.className + ' records'));

  	while (children.next()) {
  		if (deleteStrategyData.cisDeleted > deleteStrategyData.maxCIsToDelete)
  			return;
  		
  		if (isDeletionLayer) {
  			this._reconcileOnStrategy(deleteStrategyData, children.getValue(relationDirectionTo));
  			
  			deleteStrategyData.cisDeleted = deleteStrategyData.cisDeleted + 1;

  			if (deleteStrategyData.cisDeleted > deleteStrategyData.maxCIsToDelete) {
  				var warnMsg = 'Exceeded the maximum number of deletions (' + deleteStrategyData.maxCIsToDelete + ') in one pattern. Execute the pattern again to continue deleteing stale records. Increase the number property: "glide.discovery.delete_strategy.max_deletion_records" to be able to trigger more deletions in one pattern.';

  				deleteStrategyData.nodeLogger.warn(this._buildLogMessage(warnMsg));

  				if (!deleteStrategyData.inputParams.statusSysId){
  					gs.warn(warnMsg);
  					return;
  				}
  				
  				var logger = new DiscoveryLogger(deleteStrategyData.inputParams.statusSysId);
  				logger.warn(warnMsg);
  				return;
  			}
  				
  			continue;
  		}

  		var nextLevelInPath = layerToExplore.level - 1;
  		var nextLayerToExplore = {
  			level: nextLevelInPath,
  			className: deleteStrategyData.pathToBoundaryClass[nextLevelInPath],
  			parentCi: children.getValue(relationDirectionTo) + ''
  		};
  		
  		deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Further search required. Drilling down to the next layer'));
  		this._recursiveReconcileOnStrategy(deleteStrategyData, nextLayerToExplore);
  	}

  	// For related entries
  	if (isDeletionLayer) {
  		message = 'About to handle related entries for non-stale CIs';
  		deleteStrategyData.nodeLogger.info(this._buildLogMessage(message));

  		var nonStaleChildrenRelations = this._prepareChildrenQuery(relationDirectionFrom, layerToExplore.parentCi, relationDirectionTo, layerToExplore.className);
  		this._logCommonQueryConditions(deleteStrategyData.nodeLogger, relationDirectionTo);
  		this._addDeleteLayerCommonConditions(nonStaleChildrenRelations, relationDirectionTo);
  		
  		deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Adding condition: ' + relationDirectionTo + '\'s last_discovered is after ' + scheduleStartTime));
  		this._addNonStaleConditions(nonStaleChildrenRelations, relationDirectionTo, deleteStrategyData.inputParams.scheduleStartTime);
  		
  		nonStaleChildrenRelations.query();
  		
  		this._reconcileRelatedEntriesUsingReltions(deleteStrategyData, nonStaleChildrenRelations, relationDirectionTo);
  		
  		message = 'Finished handling stale related entries';
  		deleteStrategyData.nodeLogger.info(this._buildLogMessage(message));
  	}

  	return hasChildren;
  },
  
  _prepareChildrenQuery: function(source, refCi, target, childClass) {
  	var relationsGlideRecord = new GlideRecord('cmdb_rel_ci');
  	relationsGlideRecord.addQuery(source, refCi);
  	relationsGlideRecord.addQuery(target + '.sys_class_name', childClass);
  	
  	return relationsGlideRecord;
  },	

  _logCommonQueryConditions: function(nodeLogger, positionInRelation) {
  	nodeLogger.debug(this._buildLogMessage('Adding condition: ' + positionInRelation + '\'s operational status does not contain ' + JSON.stringify(this._deleteLayerExcludes())));
  	nodeLogger.debug(this._buildLogMessage('Adding condition: ' + positionInRelation + '\'s discovery_source is ' + JSON.stringify(this._deleteLayerSources())));
  },

  _reconcileRelatedEntriesUsingReltions: function(deleteStrategyData, relationsGlideRecord, relationPosition) {
  	if (!deleteStrategyData.relatedEntries || JSUtil.isEmpty(deleteStrategyData.relatedEntries )) {
  		deleteStrategyData.nodeLogger.warn(this._buildLogMessage('No related entries configuration was detected'));
  		return;
  	}
  	var cisToWorkOn = [],
  		totalStaleRelatedEntriesDeleted = 0;

  	deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Working on groups of ' + deleteStrategyData.maxCIsInSearchGroup + ' ' + deleteStrategyData.inputParams.targetCiClass));

  	while (relationsGlideRecord.next()) {
  		var sysId = relationsGlideRecord.getValue(relationPosition);
  		cisToWorkOn.push(sysId);

  		if (cisToWorkOn.length == deleteStrategyData.maxCIsInSearchGroup)
  			totalStaleRelatedEntriesDeleted += this._findAndDeleteStaleRelatedEntriesOfCIs(deleteStrategyData, cisToWorkOn);
  	}
  	
  	if (this._haveLeftOvers(cisToWorkOn.length))
  		totalStaleRelatedEntriesDeleted += this._findAndDeleteStaleRelatedEntriesOfCIs(deleteStrategyData, cisToWorkOn);
  	
  	deleteStrategyData.nodeLogger.info(this._buildLogMessage('Deleted total of ' + totalStaleRelatedEntriesDeleted + ' related entries'));
  },

  _findAndDeleteStaleRelatedEntriesOfCIs: function(deleteStrategyData, cisToWorkOn) {
  	deleteStrategyData.nodeLogger.debug(this._buildLogMessage('CIs to work on: ' + JSON.stringify(cisToWorkOn)));
  	var relatedEntriesConfigData = deleteStrategyData.relatedEntries,
  		totalStaleRelatedEntriesDeleted = 0;

  	for (var index in relatedEntriesConfigData) {
  		deleteStrategyData.nodeLogger.debug(this._buildLogMessage('About to find stale ' + relatedEntriesConfigData[index].table + ' records'));

  		var staleRelatedEntriesRecordsSysIds = this._findStaleRelatedEntriesForCIs(relatedEntriesConfigData[index], cisToWorkOn, deleteStrategyData.inputParams.scheduleStartTime);
  		deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Found total of' + staleRelatedEntriesRecordsSysIds.length + ' stale related entries'));

  		var itemsToDelete = staleRelatedEntriesRecordsSysIds.splice(0, deleteStrategyData.maxRelatedEntriesToDeleteInIteration);
  		
  		while (itemsToDelete.length > 0) {
  			deleteStrategyData.nodeLogger.debug(this._buildLogMessage('Deleting the following sysids: ' + JSON.stringify(itemsToDelete)));
  
  			this._deleteStaleRelatedEntries(relatedEntriesConfigData[index], itemsToDelete);

  			deleteStrategyData.nodeLogger.debug(this._buildLogMessage(itemsToDelete.length + ' ' + relatedEntriesConfigData[index].table + ' items were deleted'));
  			totalStaleRelatedEntriesDeleted += itemsToDelete.length;
  			
  			itemsToDelete = staleRelatedEntriesRecordsSysIds.splice(0, deleteStrategyData.maxRelatedEntriesToDeleteInIteration);
  		}
  	}
  	cisToWorkOn = [];
  	return totalStaleRelatedEntriesDeleted;
  },

  _findStaleRelatedEntriesForCIs: function(relatedEntryConfigData, cisSysIds, scheduleStartTime) {
  	var relatedEntriesSysIds = this._getAllRelatedEntriesForCIs(relatedEntryConfigData, cisSysIds);
  	
  	if (relatedEntriesSysIds.length == 0)
  		return [];
  	var staleRelatedEntriesRecordsSysIds = this._detectStaleRecordsInList(relatedEntriesSysIds, scheduleStartTime);

  	return staleRelatedEntriesRecordsSysIds;
  },

  _getAllRelatedEntriesForCIs: function(relatedEntryConfigData, cisSysIds) {
  	var relatedEntriesSysIds = [];
  	var relatedEntryGlideRecord = new GlideRecord(relatedEntryConfigData.table);
  	relatedEntryGlideRecord.addQuery(relatedEntryConfigData.referenced_field, 'IN', cisSysIds);
  	relatedEntryGlideRecord.query();

  	while (relatedEntryGlideRecord.next())
  		relatedEntriesSysIds.push(relatedEntryGlideRecord.getUniqueValue());
  	return relatedEntriesSysIds;
  },

  _detectStaleRecordsInList: function(relatedEntriesSysIds, scheduleStartTime) {
  	var staleRecordsSysIds = [];

  	var sysObjectSourceGlideRecord = new GlideRecord('sys_object_source');
  	sysObjectSourceGlideRecord.addQuery('name', this.discoverySource);
  	sysObjectSourceGlideRecord.addQuery('sys_updated_on', '<', scheduleStartTime);
  	sysObjectSourceGlideRecord.addQuery('target_sys_id', 'IN', relatedEntriesSysIds);
  	sysObjectSourceGlideRecord.query();
  	
  	while (sysObjectSourceGlideRecord.next())
  		staleRecordsSysIds.push(sysObjectSourceGlideRecord.getValue('target_sys_id'));
  	return staleRecordsSysIds;
  },

  _deleteStaleRelatedEntries: function(relatedEntryConfigData, staleRecordsSysIds) {
  		var relatedEntryGlideRecord = new GlideRecord(relatedEntryConfigData.table);
  		relatedEntryGlideRecord.addQuery('sys_id', 'IN', staleRecordsSysIds);
  		relatedEntryGlideRecord.query();

  		relatedEntryGlideRecord.deleteMultiple();
  },
  
  _haveLeftOvers: function(itemsToHandle) {
  	return itemsToHandle > 0;
  },

  // reconcile the CI
  _reconcileOnStrategy: function(deleteStrategyData, ciRef) {
  	var ci = new GlideRecord(deleteStrategyData.inputParams.targetCiClass);

  	if (!ci.get('sys_id', ciRef)) {
  		var logger = new DiscoveryLogger(deleteStrategyData.inputParams.statusSysId);
  		var message = 'No CI with sysid ' + ciRef + ' was found. Skipping deletion';

  		deleteStrategyData.nodeLogger.warn(this._buildLogMessage(message));
  		logger.warn(message);
  
  		return;
  	}
  	
  	// remove all related entries first
  	if (deleteStrategyData.relatedEntries) {
  		for (var counter = 0; counter < deleteStrategyData.relatedEntries.length; counter ++) {
  			var relatedEntry = deleteStrategyData.relatedEntries[counter];
  			var relatedEntryCleanerGr = new GlideRecord(relatedEntry.table);
  			relatedEntryCleanerGr.addQuery(relatedEntry.referenced_field, ciRef);
  			relatedEntryCleanerGr.deleteMultiple();
  		}
  	}

  	var targetClassDeleteStrategy = deleteStrategyData.inputParams.ciTypeToStrategy.get(deleteStrategyData.inputParams.targetCiClass);
  	var deleteCI = this._getDeleteStrategyFunction(targetClassDeleteStrategy);

  	deleteCI(ci, deleteStrategyData.inputParams.scheduleStartTime);

  	var children = this._getChildren(ci, deleteStrategyData.inputParams.scheduleStartTime);

  	if (children.length > 0) {
  		gs.info('Processing ' + children.length + ' stale children of CI : ' + ciRef);

  		for (var index = 0; index < children.length; index++) {
  			var childCi = children[index];
  			var childCiRef = new GlideRecord(childCi.table);

  			if (childCiRef.get('sys_id', childCi.sysId)) {
  				var preferredStrategy = deleteStrategyData.inputParams.ciTypeToStrategy.get(childCi.table) ? deleteStrategyData.inputParams.ciTypeToStrategy.get(childCi.table) : targetClassDeleteStrategy;

  				deleteCI = this._getDeleteStrategyFunction(preferredStrategy);
  				deleteCI(childCiRef, deleteStrategyData.inputParams.scheduleStartTime);
  			}
  		}
  	}
  },
  
  _getDeleteStrategyFunction: function(deleteStrategyCode) {
  	return this.deleteStrategies[deleteStrategyCode].deletionFunction;
  },

  _findRelatedEntries: function(ciClass) {
  	var relatedEntries = [];

  	// process the related entries for the stale CI
  	var relatedEntriesGr = new GlideRecord('cmdb_related_entry');
  	var relJoinGr = relatedEntriesGr.addJoinQuery('cmdb_identifier', 'identifier', 'sys_id');
  	relJoinGr.addCondition('applies_to', ciClass);
  	relatedEntriesGr.query();

  	while (relatedEntriesGr.next()) {
  		var relatedEntry = {};
  		relatedEntry.table = relatedEntriesGr.getValue('table');
  		relatedEntry.referenced_field = relatedEntriesGr.getValue('referenced_field');
  		relatedEntries.push(relatedEntry);
  	}

  	return relatedEntries;
  },

  // mark a CI as absent
  _markAbsent: function(ci) {
  	if (!ci.isValidField('operational_status') || !ci.isValidField('install_status'))
  		return;

  	gs.info('Marking CI ' + ci.sys_id + ' as absent');

  	if (ci.isValidField('state'))
  		ci.state = 'terminated';

  	ci.operational_status = '2'; // non-operational
  	ci.install_status = '100'; //absent
  	ci.update();
  },

  // mark a CI as retired
  _markRetired: function(ci) {
  	if (!ci.isValidField('operational_status') || !ci.isValidField('install_status'))
  		return;

  	gs.info('Marking CI ' + ci.sys_id + ' as retired');

  	if (ci.isValidField('state'))
  		ci.state = 'terminated';

  	ci.operational_status = '6'; // retired
  	ci.install_status = '7'; // retired
  	ci.update();
  },

  // delete a CI
  _deleteCI: function(ci, scheduleStartTime) {
  	this._deleteRelations(ci, scheduleStartTime);

  	gs.info('Deleting CI ' + ci.sys_id + ' from CMDB');
  	ci.deleteRecord();
  },

  // delete all relations for this CI
  _deleteRelations: function(ci, scheduleStartTime) {
  	// for a CI that's being updated to stale, find the relationships that it's referenced in and
  	// remove that if the timestamp is not after the discovery schedule launch time
  	gs.info('Removing stale relationships for CI : ' + ci.sys_id);
  	var relationsGr = new GlideRecord('cmdb_rel_ci');
  	relationsGr.addQuery('parent', ci.sys_id);
  	relationsGr.addQuery('sys_updated_on', '<=', scheduleStartTime);
  	relationsGr.deleteMultiple(); // remove the stale relationship from the CMDB
  },

  // reconcile the children of a specific CI
  _getChildren: function(ci, scheduleStartTime) {
  	var children = [];
  	var childRel = new GlideRecord('cmdb_rel_ci');
  	childRel.addQuery('parent', ci.sys_id);
  	childRel.addQuery('type.name', 'Contains::Contained by');
  	childRel.addQuery('child.operational_status', 'NOT IN', this._deleteLayerExcludes()); //filter out already reconciled CIs
  	childRel.addQuery('child.last_discovered', '<=', scheduleStartTime);
  	childRel.query();

  	while (childRel.next()) {
  		var childCI = {};
  		childCI.sysId = childRel.getValue('child');
  		childCI.table = childRel.child.sys_class_name + '';
  		children.push(childCI);
  	}

  	return children;
  },
  
  _getPathBetweenClasses: function(targetClass, sourceClass, path, level) {
  	if (level == null)
  		level = 0;
  	
  	if (level > 4)
  		return null;
  	
  	if(sourceClass == null || sourceClass == '')
  		return null;
  	
  	var newPath = [];
  	if (path != null)
  		path.forEach(function(item) { newPath.push(item); } ); 
  	
  	newPath.push(sourceClass);
  	
  	var classHierarchy = SNC.ClassModel.getClassHierachy(sourceClass, 'cmdb_ci');
  	
  	// Check host parents
  	var hostWalk = new GlideRecord('cmdb_metadata_hosting');
  	hostWalk.addQuery('parent_type', 'IN', classHierarchy);
  	hostWalk.addQeury('rel_type.name', 'Hosted on::Hosts');
  	hostWalk.query();
  	
  	var possibleRelationPaths = [];
  	
  	while (hostWalk.next()) {			
  		if (SNC.ClassModel.isInstanceOf(targetClass, hostWalk.child_type)) {
  			newPath.push(hostWalk.child_type);
  			return newPath;
  		}
  		
  		possibleRelationPaths.push(hostWalk.child_type);
  	}
  	
  	for (var i = 0; i < possibleRelationPaths.length; i++) {
  		var hostChildPathResult = this._getPathBetweenClasses(targetClass, possibleRelationPaths[i], newPath, level + 1);
  		if (hostChildPathResult != null)
  			return hostChildPathResult;
  	}
  	
  	// Check contains parents
  	var containWalk = new GlideRecord('cmdb_metadata_containment');
  	containWalk.addQuery('ci_type', 'IN', classHierarchy);
  	containWalk.addQeury('rel_type.name', 'Contains::Contained by');
  	containWalk.query();
  	
  	possibleRelationPaths = [];
  	
  	while (containWalk.next()) {
  		if (SNC.ClassModel.isInstanceOf(targetClass, containWalk.parent_id.ci_type)) {
  			newPath.push(containWalk.parent_id.ci_type);
  			return newPath;
  		}
  		
  		possibleRelationPaths.push(containWalk.parent_id.ci_type);
  	}
  	
  	for (i = 0; i < possibleRelationPaths.length; i++) {
  		var containChildPathResult = this._getPathBetweenClasses(targetClass, possibleRelationPaths[i], newPath, level + 1);
  		if (containChildPathResult != null)
  			return containChildPathResult;
  	}
  	
  	return null;
  },
  
  _getRecordByID: function(sysId) {
  	var ciRecord = new GlideRecord('cmdb_ci');
  	ciRecord.get(sysId);
  	
  	return ciRecord;
  },
  
  _addStaleConditions: function(currentQuery, positionInRelation, scheduleStartTime) {
  	currentQuery.addQuery(positionInRelation + '.last_discovered', '<=', scheduleStartTime);
  },
  
  _addNonStaleConditions: function(currentQuery, positionInRelation, scheduleStartTime) {
  	currentQuery.addQuery(positionInRelation + '.last_discovered', '>', scheduleStartTime);
  },

  _addDeleteLayerCommonConditions: function(currentQuery, positionInRelation) {
  	currentQuery.addQuery(positionInRelation + '.operational_status', 'NOT IN', this._deleteLayerExcludes());
  	currentQuery.addQuery(positionInRelation + '.discovery_source', 'IN', this._deleteLayerSources());
  },

  _enhanceDeleteLayerQuery: function(currentQuery, target, scheduleStartTime) {
  	return currentQuery;
  },

  _deleteLayerExcludes: function() {
  	return ['2', '6'];
  },
  
  _deleteLayerSources: function() {
  	return ['ServiceNow'];
  },

  _getDeleteStrategyCodeFromData: function(deleteStrategyData) {
  	return deleteStrategyData.inputParams.ciTypeToStrategy.get(deleteStrategyData.inputParams.targetCiClass);
  },

  _getStrategyNameByCode: function(deleteStrategyCode) {
  	return this.deleteStrategies[deleteStrategyCode].description || 'Unknown'
  },

  _buildLogMessage: function(message) {
  	return AbstractDeleteStrategy.LOGGER_NAME_LOG_PREFIX + '[' + this._getScriptTableName() + '_' + this._getScriptSysId() + '] ' + message;
  },

  _getScriptTableName: function() {
  	return 'sys_script_include';
  },

  _getScriptSysId: function() {
  	return '591e87c1db690010690cf9c31d9619f7';
  },

  type: 'AbstractDeleteStrategy'
};

// A template to create a new strategy script
AbstractDeleteStrategy.deleteStrategyTemplate = 'var sysparm_delete_strategy_name = Class.create(); \
\
sysparm_delete_strategy_name.prototype = Object.extendsObject(global.AbstractDeleteStrategy, { \
\
  /* \
   * Override if you want to implement your own custom search logic \
   * This should return an array of the sysids to delete \
   * \
   * _getCIs: function(scheduleStartTime, ciClass, patternInputs, patternContext, nodeLogger) { \
   * \t// To use nodeLogger: nodeLogger.[info\\warn\\error\\debug](this._buildLogMessage(message)); \
   * }, \
   */ \
\
  /* \
   *_getRelationshipPaths: function() { \
   *  // a sublass can provide additional relationship paths to walk down ex: \
   * \
   *  var additionalRelationPaths = {}; \
   * \
   *  additionalRelationPaths[\'cmdb_ci_endpoint_block\'] = [ \
   *     \'cmdb_ci_endpoint_block\', \'cmdb_ci_storage_volume\', \'cmdb_ci_aws_datacenter\' \
   *  ]; \
   *  \
   *  return additionalRelationPaths; \
   * }, \
   */ \
\
  // Given a pattern\'s context, return the boundaries needed to identify stale CI\'s \
  // MUST be implemented! \
  _getBoundaryConditions: function(patternContext, nodeLogger) { \
  	// To use nodeLogger: nodeLogger.[info\\warn\\error\\debug](this._buildLogMessage(message)); \
  	var boundaries = []; \
      return boundaries; \
  }, \
\
  _getScriptTableName: function() { \
  	return \'sn_discovery_delete_strategy\'; \
  }, \
\
  _getScriptSysId: function() { \
  	return \'sysparm_delete_strategy_sysid\'; \
  }, \
\
  type: \'sysparm_delete_strategy_name\' \
});';

Sys ID

591e87c1db690010690cf9c31d9619f7

Offical Documentation

Official Docs: