Name

global.CloneAPI

Description

Instance clone client helper API to schedule/start/rollback a clone. The client fires web service requests to a central scheduling/automation service on DataCenter. Customizations to this script will be overwritten on plugin update.

Script

var CloneAPI = Class.create();
CloneAPI.prototype = {
  initialize: function() {
  this._errorMessage = null;
  this.className = "CloneAPI";
  this.maxRetryCount = 3;
},

type: 'CloneAPI',

isOracleClone: function() {
  var isOracle = false;
  var dbi = GlideDBConfiguration.getDBI("sys_dictionary");
  isOracle = dbi.isOracle();
  dbi.close();
  return isOracle;
},

isInAppCloneEligible: function(cloneGr) {
  var cloneId = cloneGr.getValue('clone_id');
  if(this.isOracleClone()) {
    gs.logError(cloneId + ": Fallback to in-app clone not supported for Oracle database", this.className);
    return false;
  }
  // provide property to explicitly block in-app clone for mysql based cloning
  var fallbackProp = 'glide.db.clone.fallback_inapp';
  var canFallback = gs.getProperty(fallbackProp, false);
  if (!canFallback || canFallback == 'false' || canFallback == '0') {
    gs.log( cloneId + ": Fallback to in-app clone disabled", this.className );
    return false;
  }
  var isSameVersion = this.isSameVersion(cloneGr.getValue("target_instance"));
  if(!isSameVersion) {
    gs.log(cloneId + ": " + this._errorMessage, this.className);
    return false;
  }
  if(this.isInAppCloneRunning()) {
    gs.log( cloneId + ": Clone cannot be started - an active clone is already running", this.className );
    return false;
  }
  this.logCloneMessage(cloneGr, /*info*/0, 'clone.log.info.inapp.eligible', cloneGr.getValue('clone_id'));
  return true;
},

scheduleInAppClone: function(cloneGr) {
  var cloneType = cloneGr.getValue('clone_type');
  var cloneId = cloneGr.getValue('clone_id');
  if(cloneType.equalsIgnoreCase('in-app') && this.isInAppCloneEligible(cloneGr)) {
    gs.log(cloneId + ': Starting in-app clone.', this.className);
    this.logCloneMessage(cloneGr, /*info*/0, 'clone.log.info.inapp.started', cloneGr.getValue('clone_id'));
    this._ensurePreferredNode(cloneGr);
    SncInstanceClone.startCloneViaClusterMessage(cloneGr);
  }
},

scheduleInstanceClone: function(table, sysId, retryCount) {
  var cloneGr = new GlideRecord(table);
  if ( !cloneGr.get(sysId) ) {
    gs.logError( "Clone record " + sysId + " not found in " + table, this.className );
    return;
  }
  var cloneId = cloneGr.getValue('clone_id');
  if(gs.nil(retryCount)) {
    retryCount = 0;
    // Fetch the war version from target and store it in source db
    this._updateWarVersion(cloneGr.getValue("target_instance"));
  }
  var notifiedServer = this._notifyServer(cloneGr);
  if(!notifiedServer) {
    gs.log(cloneId + ': Clone Scheduling failed. Retrying the scheduling.', this.className);
    this.retryInstanceCloneSchedule(table, sysId, ++retryCount);
  }
},

retryInstanceCloneSchedule: function(table, sysId, retryCount) {
  var cloneGr = new GlideRecord(table);
  if ( !cloneGr.get(sysId) ) 
    return;

  var cloneId = cloneGr.getValue('clone_id');
  if(!gs.nil(retryCount) && retryCount < this.maxRetryCount) {
    var waitingTimeInMins = parseInt((Math.pow(2,retryCount) - 1));
    var nextTime = new GlideDateTime();
    nextTime.addSeconds(60 * waitingTimeInMins);
    var schedule = new ScheduleOnce();
    schedule.setLabel( "Poll for schedule clone " + cloneId );
    schedule.setTime( nextTime );
    schedule.setSystemID( this._getSystemID(table, sysId) );
    schedule.script = "new CloneAPI().scheduleInstanceClone('" + table + "','" + sysId + "'," + retryCount + ");";
    var jobId = schedule.schedule();
    gs.log('Scheduled retry for clone ' + cloneId + ' after ' + waitingTimeInMins + ' mins with job id ' + jobId, this.className);
    return;
  }

  if(this.isInAppCloneEligible(cloneGr)) {
    gs.log('Converting clone ' + cloneId + ' to in-app clone as scheduling as backup based failed.', this.className);
    cloneGr.setValue('clone_type', 'in-app');
    cloneGr.setValue('state','Scheduled');
  } else {
    this.logCloneMessage(cloneGr, /*error*/2, 'clone.log.error.schedule.failure', cloneGr.getValue('clone_id'));
    gs.log('Max Retry limit reached for clone ' + cloneId + '. Marking the state as Hold.', this.className);
    cloneGr.setValue('state', 'Hold');
    cloneGr.setWorkflow(false);
  }

  cloneGr.update();
},

logCloneMessage: function(cloneGr, logLevel, msgKey, msgArgs) {

  var argsArray = [];
  
  if(gs.nil(cloneGr)) 
    return;

  if(!gs.nil(msgArgs)) 
    argsArray.push(msgArgs);

  SncCloneLogger.log(this.className, cloneGr.getValue('sys_id'), null, logLevel, gs.getMessage(msgKey, argsArray));
},

cancelClone: function(cloneGr) {
  var cloneType = cloneGr.getValue("clone_type");
  var cloneId = cloneGr.getValue('clone_id');
  var cloneSysId = cloneGr.getValue('sys_id');

  // Backup based clone
  if (gs.nil(cloneType) || cloneType.equalsIgnoreCase("backup-based")) {
    var jobId = new InstanceCloneScheduler().notifySchedulingServerForCancel(cloneGr);
    gs.log("Cancel Clone Scheduled with Job sysID " + jobId + " for Clone : " + cloneId, this.className);

    return !gs.nil(jobId);
  }

  // In-app based clone
  if (cloneType.equalsIgnoreCase("in-app")) {
    var instanceClone = new SncInstanceClone(cloneSysId);
    gs.log("Canceling in-app clone with clone record : " + cloneId, this.className);

    try {
      SncInstanceClone.stopCloneViaClusterMessage(cloneSysId);
      return true;
    } catch (e) {
      gs.log("Cancel Clone for in-app Clone " + cloneId + " Failed with exception " + e.getMessage(), this.className);
      return false;
    } 
  }
},

rollbackClone: function(cloneGr) {
  var cloneType = cloneGr.getValue("clone_type");
  var cloneId = cloneGr.getValue('clone_id');
  var cloneSysId = cloneGr.getValue('sys_id');

  // Backup based clone
  if (gs.nil(cloneType) || cloneType.equalsIgnoreCase("backup-based")) {
    var jobId = new InstanceCloneScheduler().notifySchedulingServerForRollback(cloneGr);
    gs.log("Rollback Clone Scheduled with Job sysID " + jobId + " for Clone : " + cloneId, this.className);

    return !gs.nil(jobId);
  }
  
  // In-app based clone
  if (cloneType.equalsIgnoreCase("in-app")) {
    var instanceClone = new SncInstanceClone(cloneSysId);
    gs.log("Rolling back in-app clone with clone record : " + cloneId, this.className);

    try {
      if (!instanceClone.canRollback()) {
        gs.log("Clone : " + cloneId + " cannot be rolled back.", this.className);
        return false;
      }

      SncInstanceRollback.rollback(cloneSysId);
      return true;
    } catch (e) {
      gs.log("Rollback Clone for in-app Clone " + cloneId + " Failed with exception " + e.getMessage(), this.className);
      return false;
    } 
  }
},

_ensurePreferredNode: function(gr) {
  var clusterNode = gr.getValue("cluster_node");
  if (!gs.nil(clusterNode))
    return;
  
  var instanceId = gr.getValue("target_instance");
  gs.log("CloneAPI: ensurePreferredNode against target instance " + instanceId);
  var nodeList = this.getNodesOnlineByLatency(instanceId);
  if (nodeList == null)
        return;
  
  clusterNode = nodeList[0];
  gs.log("CloneAPI: ensurePreferredNode against target instance " + instanceId + " got best node " + new global.JSON().encode(clusterNode));
  
  var clusterNodeID = typeof clusterNode === 'string' ? clusterNode : clusterNode.sys_id;
  gr.setValue("cluster_node", clusterNodeID);
  gr.setWorkflow(false); // don't fire business rules; we're updating some metadata fields
  gr.update();
},

getNodesOnlineByLatency: function(instanceId) {
  var sleepTime = 2000; // 2s
  var maxWait = 60000;  // max we'll wait is 60 seconds (60,000 milliseconds)
  var shouldWait = true;

  var checkGroup = this.runConnectionTest(instanceId, true);

  if (!checkGroup)
      return null;

  gs.sleep(sleepTime); // wait for cluster nodes to pick up message
  var startTime = new Date().getTime();

  while (shouldWait) {
      var currentTime = new Date().getTime();

      if ((currentTime - startTime) > maxWait) {
          gs.log("Max wait time reached when finding closest node... Exiting");
          break;
      }
      var gr = new GlideRecord('ha_connectivity_test');
      gr.addQuery('check_group', checkGroup);
      gr.addQuery('latency', '');
      gr.query();

      // if we're not waiting on any results, return the lowest latency
      if (gr.next() == false) {
          gs.log("CloneAPI: Results done - finding node with lowest latency...");
          return new HAAPIs()._findLowLatency(checkGroup);
      }

      gs.sleep(sleepTime); // wait 3 seconds
  }

  gs.log("Max wait time reached, not all nodes checked in. Returning fastest successful node...");
  return new HAAPIs()._findLowLatency(checkGroup, true);
},

runConnectionTest: function(instanceId) {
  var checkGroup = gs.generateGUID();
  var nodeList = new HAAPIs().getNodesOnline();
  for(var i = 0; i < nodeList.length; i++) {
      var node = nodeList[i];
      var ctgr = new GlideRecord("ha_connectivity_test");
      ctgr.state = 1;
      ctgr.check_group = checkGroup;
      ctgr.message = "Starting";
      ctgr.fast_check = true;
      ctgr.target_instance = instanceId;
      ctgr.cluster_node = node.sys_id;
      ctgr.insert();
  }
  return checkGroup;
},

isDBValid: function(instanceid) {
  return true;
},

getErrorMessage: function() {
  return this._errorMessage;
},

isCloneRunning: function() {
  return SncCloneUtils.isCloneRunning();
},

isInAppCloneRunning: function() {
  var cloneGr = new GlideRecord("clone");
  cloneGr.addQuery("state","Active");
  cloneGr.addQuery("clone_type","in-app");
  cloneGr.query();
  if (cloneGr.next())
    return true;
  else
    return false;
},

isSameVersion: function(instanceId) {
  var igr = new GlideRecord("instance");
  if ( !igr.get(instanceId) ) {
    gs.logError( "Instance record " + instanceId + " not found" );
    return false;
  }

  var remoteVersion = igr.getValue("war_version") + "";
  var myVersion = gs.getProperty("glide.war") + "";
  if ( !this._isSameVersion(myVersion, remoteVersion) ) {
    this._errorMessage = 'Instance "' + igr.instance_name + '" is currently on version "' + remoteVersion + '". The target instance needs to be upgraded to version "' + myVersion + '" before a clone request can be submitted.';
    return false;
  }

  return true;
},

_isSameVersion: function(myVersion, remoteVersion) {
  gs.log("CloneAPI isSameVersion: remoteVersion=" + remoteVersion + " vs myVersion=" + myVersion);

  if (!gs.nil(remoteVersion) && remoteVersion == 'null')
      remoteVersion = null;

  if (!gs.nil(myVersion) && myVersion == 'null')
      myVersion = null;

  if (gs.nil(remoteVersion) && gs.nil(myVersion))
      return true;

  if (!gs.nil(myVersion) && myVersion.indexOf('.') > -1)
      myVersion = myVersion.substring(0, myVersion.length-4);

  if (!gs.nil(remoteVersion) && remoteVersion.indexOf('.') > -1)
      remoteVersion = remoteVersion.substring(0, remoteVersion.length-4);

  if (myVersion == remoteVersion)
      return true;
  
  return false;
},

_beforeStartClone: function(cloneRecord) {
  // Fetch the war version from target and store it in source db
  this._updateWarVersion(cloneRecord.getValue("target_instance"));

  // contact clone web service
  var notifiedServer = this._notifyServer(cloneRecord);
  return notifiedServer;
},

_updateWarVersion: function(instanceId) {
  if (gs.nil(instanceId))
    return;

  var instanceGR = new GlideRecord("instance");
  if ( !instanceGR.get(instanceId) ) {
    gs.logError( "CloneAPI: Instance record " + instanceId + " not found in instance table" );
    return;
  }
  instanceGR.setWorkflow(false); // don't fire business rules; we're updating some metadata fields

var instanceUrl = instanceGR.getValue("instance_url");
var instanceName = instanceGR.getValue("instance_name");
var user = instanceGR.getValue("admin_user");
var secret = instanceGR.getValue("admin_password");
var clearText = new GlideEncrypter().decrypt(secret);

  // Fetch the war version from target and store it in source db
try {
  var version = this._getWarVersion(instanceUrl, user, clearText);
  instanceGR.setValue("war_version", version);
  instanceGR.setValue("validation_error", "");
  gs.log("Retrieved instance version: " + instanceName + " = " + version);
} catch(e) {
  instanceGR.setValue("validation_error", "Failed to retrieve instance version: " + e.toString());
  gs.warn("Failed to obtain instance version: " + instanceName);
}
instanceGR.update();
},

_getWarVersion: function(instanceUrl, user, pwd) {
var getPropertyUrl = this._buildWSURL(instanceUrl, "GetProperty.do?SOAP");    

var soapdoc = new SOAPEnvelope("GetProperty", "http://www.service-now.com/");
soapdoc.setFunctionName("execute");
soapdoc.addFunctionParameter("property", "glide.war");

var soapRequest = new GlideInternalSoapClient(getPropertyUrl, user, pwd);
soapRequest.setSOAPAction(soapdoc.functionName);
soapRequest.postRequest(soapdoc.toString());
var xmlStr = soapRequest.getResponseXML();
gs.log("CloneAPI._getWarVersion getHTTPStatus=" + soapRequest.getHTTPStatus());   
gs.log("CloneAPI._getWarVersion == " + xmlStr);

if (soapRequest.getHTTPStatus() == 200) {
  var property = gs.getXMLText(xmlStr, "//executeResponse/property");
  if (!gs.nil(property))
    return property;
}

throw "httpStatus == " + soapRequest.getHTTPStatus() + ", httpResponse == " + xmlStr;
},

_buildWSURL: function(instance_url, page) {
var url = instance_url+"";
var http = "";
var slash = "";
if (url.charAt(url.length-1, 1) != '/')
  slash = "/";

if (url.indexOf("http") != 0) {
  http = "http://";
  if (url.indexOf("localhost") == -1)
    http = "https://";
}

return http + url + slash + page;
},

_notifyServer: function(cloneRecord) {
  var answer = false;
  try {
    if (new InstanceCloneScheduler().notifyServer(cloneRecord))
  	  answer = true;
  } catch(e) {
     SncCloneLogger.log("InstanceClone", cloneRecord.sys_id, null, /*warn*/1, "Unable to contact server for confirmation: " + e.description);
  }
  return answer;
}, 

_isReadyState: function(cloneRecord) {
  return cloneRecord.getValue('state') == 'Requested' || cloneRecord.getValue('state') == 'Scheduled';
},

_isRequestedState: function(cloneRecord) {
  return cloneRecord.getValue('state') == 'Requested';
},

restartClone: function(cloneRecord) {
  var table = cloneRecord.sys_class_name+'';
  if( table != 'clone_instance' ) {
    gs.addErrorMessage( "Unable to restart clone: only Instance Clone records can be restarted (expected table 'clone_instance', actual table '" + table + "')" );
    return false;
  }
  
  var newCloneId = this._copyCloneRecord(cloneRecord);
  gs.log("CloneAPI: restartClone created new clone_instance record '" + newCloneId + "'");
  return newCloneId !== null;
},      

_copyCloneRecord: function(model) {
  var newClone = new GlideRecord("clone_instance");
  newClone.initialize();
  newClone.name = model.name;
  newClone.source_instance = model.source_instance; 
  newClone.target_instance = model.target_instance;
  newClone.exclude_large_data = model.exclude_large_data; 
  newClone.preserve_theme = model.preserve_theme; 
  newClone.filter_attachment_data = model.filter_attachment_data;
  newClone.security_token = model.security_token; 
  newClone.cluster_node = model.cluster_node; 
  newClone.email = model.email;
  newClone.setValue("scheduled", new GlideDateTime()); // now GMT
  newClone.state = 'Requested';
  newClone.megabytes_to_copy = model.megabytes_to_copy;
  newClone.duration = model.duration;
  newClone.retries = Math.max(model.retries, 0) + 1; // indicates this is a restart
  var newCloneId = newClone.insert();

  var sourceCloneId = model.getUniqueValue();
  this._copyPreservedData(sourceCloneId, newCloneId);

  return newCloneId;
},
  
_copyPreservedData: function(sourceCloneId, targetCloneId) {
  var preservedCount = 0;
  var grSourceData = new GlideRecord("clone_preserved_data");
  grSourceData.addQuery("clone", sourceCloneId);
  grSourceData.query();
  while (grSourceData.next()) {
    ++preservedCount;

    var payload = grSourceData.getValue("payload");
    var newPreservedData = new GlideRecord("clone_preserved_data");
    newPreservedData.initialize();
    newPreservedData.clone = targetCloneId;
    newPreservedData.payload = payload;
    newPreservedData.insert();
  }
  gs.print("CloneAPI: inserted " + preservedCount + " preserved data records for restarted clone " + targetCloneId);
},
 
_getSystemID: function(table, sysId) {
    var systemID = null;
  var cloneRecord = new GlideRecord(table);
  if (cloneRecord.get(sysId)) {
    var preferredNode = cloneRecord.getValue("cluster_node");
    if (!gs.nil(preferredNode)) {
        var clusterStateRecord = GlideClusterSynchronizer.getNodeById(preferredNode);
      if (clusterStateRecord != null && clusterStateRecord.status == "online")
        systemID = clusterStateRecord.system_id;
    }
  }
  return systemID;
}
};

Sys ID

f595e0101b23100081599e3bcc071392

Offical Documentation

Official Docs: