Name

global.AvailabilityCalculator

Description

This script does availability calculation given a start and an end date, it considers the availability as well as maintenance commitments along with the schedules attached to the offering.

Script

var AvailabilityCalculator = Class.create();

AvailabilityCalculator.prototype = {
  SCHEDULES: new GlideLRUCache(50),
  UTILS: new global.AvailabilityUtils(),

  initialize: function() {
      this.cmdb_ci = null;
      this.commitment = null;
  },

  calculate: function(start, end, type) {
      type = type || 'daily';
      var commits = this._getCommits();
      while (commits.next()) {
          // Check legacy edge case where "service_offering_commitment.service_offering" is a reference to "cmdb_ci"
          // AND service_offering is state = draft
          var commitmentOffering = commits.service_offering;
          if (commitmentOffering) {
              var commitmentCiClass = commitmentOffering.sys_class_name;
              if (commitmentCiClass == 'service_offering' && commitmentOffering.state == 'draft')
                  continue;
          }

          var schedule = commits.service_commitment.schedule + '';
          var tz = commits.service_commitment.timezone + '';
          var cmdb_ci = this.UTILS.getCiFromCommitment(commits);

          var outages = this._getOutages(start, end, cmdb_ci);

          this.sumCount = 0;
          var absolute = this._sumOutages(outages, null);
          var absolute_count = this.sumCount;

          this.sumCount = 0;
          var scheduled = this._sumOutages(outages, this._getSchedule(schedule, tz, cmdb_ci));
          var scheduled_count = this.sumCount;

          var delta = new Array();
          var dp = new Object();
          dp.start = start.getNumericValue();
          dp.end = end.getNumericValue();
          delta.push(dp);

          var absolute_total = this._sumOutages(delta, null);
          var scheduled_total = this._sumOutages(delta, this._getSchedule(schedule, tz, cmdb_ci));

          var absolute_avail = 100 * ((absolute_total.getNumericValue() - absolute.getNumericValue()) / absolute_total.getNumericValue());

          var scheduled_avail = 100;
          if (scheduled_total.getNumericValue() != 0)
              scheduled_avail = 100 * ((scheduled_total.getNumericValue() - scheduled.getNumericValue()) / scheduled_total.getNumericValue());

  		var mtbf = scheduled_total.getNumericValue();
          var mtrs = 0;
          if (scheduled_count != 0) {
              mtbf = (scheduled_total.getNumericValue() - scheduled.getNumericValue()) / scheduled_count;
              mtrs = scheduled.getNumericValue() / scheduled_count;
          }

          // allowed downtime is downtime that we're allowed under this schedule (might be zero)
          var allowed_downtime = scheduled_total.getNumericValue() * ((100 - commits.service_commitment.availability) / 100);

          mtbf = new GlideDuration(mtbf);
          mtrs = new GlideDuration(mtrs);
          allowed_downtime = new GlideDuration(allowed_downtime);

          var met = scheduled.getNumericValue() <= allowed_downtime.getNumericValue();

          // actually log it
          var ar = new AvailabilityRecord(cmdb_ci, start, end);
  	    ar.setCiClass(this.UTILS.getCiClassFromCommitment(commits));
          ar.setType(type);
          ar.post(commits.service_commitment, absolute, scheduled, absolute_avail, scheduled_avail, absolute_count, scheduled_count, scheduled_total, mtbf, mtrs, allowed_downtime, met);
      }
  },

  _getSchedule: function(s, tz, cmdb_ci) {
      var schedule = this._getSchedule0(s, tz);
      var maint = this._getMaint(cmdb_ci);
      if (!maint)
          return schedule;

      if (!schedule)
          schedule = this._getFullDay(tz);

      for (var i = 0; i < maint.length; i++)
          schedule.addOtherSchedule0(maint[i], false);

      return schedule;
  },

  _getSchedule0: function(s, tz) {
      if (!s)
          return null;

      var answer = this.SCHEDULES.get(s);
      if (!answer) {
          answer = new GlideSchedule(s);
          this.SCHEDULES.put(s, answer);
      }

      // copy this in case we add a maintenance schedule later (munges the schedule itself)
      answer = new GlideSchedule(answer);
  	if (tz)
  		answer.setTimeZone(tz);
      return answer;
  },

  _getMaint: function(cmdb_ci) {
      var commits = new GlideRecord('service_offering_commitment');
      commits.addQuery('service_commitment.type', 'maintenance_window');
      commits.addQuery('service_offering', cmdb_ci).addOrCondition('cmdb_ci', cmdb_ci);
      commits.addNotNullQuery('service_commitment.schedule');
      commits.query();
      if (!commits.hasNext())
          return null;

      var answer = new Array();
      while (commits.next()) {
          var sched = commits.service_commitment.schedule;
          var tz = commits.service_commitment.timezone;
          answer.push(this._getSchedule0(sched, tz));
      }
      return answer;
  },

  _sumOutages: function(outages, schedule) {
      var sum = 0;
      if (schedule) {
          var start = new GlideDateTime();
          var end = new GlideDateTime();
      }

      for (var i = 0; i < outages.length; i++) {
          var dur = 0;
  		// ONLY COUNT OUTAGES WITHOUT TYPE OR TYPE IS OUTAGE, IGNORE OTHER TYPES
  		if (!outages[i].type || outages[i].type == 'outage') {
  			if (schedule) {
  				start.setNumericValue(outages[i].start);
  				end.setNumericValue(outages[i].end);
  				dur = schedule.duration(start, end).getNumericValue();
  			} else
  				dur = (outages[i].end - outages[i].start);
  		}
          sum += dur;
          if (dur > 0)
              this.sumCount++;
      }

      var answer = new GlideDuration();
      answer.setNumericValue(sum);
      return answer;
  },

  _getCommits: function() {
      var commits = new GlideRecord('service_offering_commitment');
      commits.addQuery('service_commitment.type', 'availability');

  	// ensure that there is either a configuration item populated, or a service offering that is in published state
  	if (this._hasOfferingReference())
  		commits.addEncodedQuery('cmdb_ciISNOTEMPTY^ORservice_offering.state=published^ORservice_offering.state=');		

      if (this.cmdb_ci != null)
          commits.addQuery('service_offering', this.cmdb_ci).addOrCondition('cmdb_ci', this.cmdb_ci);
      if (this.commitment != null)
          commits.addQuery('service_commitment', this.commitment);  
  	commits.query();
      return commits;
  },

  _hasOfferingReference: function() {
  	var gr = new GlideRecord("sys_dictionary"); 
  	gr.addQuery("name", "service_offering_commitment");
  	gr.addQuery("element", "service_offering");
  	gr.addQuery("reference", "service_offering");
  	gr.query();
  	
  	//Check to see if reference is already set to service_offering
  	if (gr.hasNext())
  		return true;

  	return false;
  },

  _getOutages: function(start /* GlideDateTime */ , end /* GlideDateTime */ , cmdb_ci /* String */ ) {
  	var outages = new Array();
  	var sd = start.getNumericValue();
  	var ed = end.getNumericValue();

  	// QUERY FOR OUTAGES WITH MATCHING CI FROM OUTAGE TABLE
  	// this is for historical data that did not use the cmdb_outage_ci_mtom table
  	var gr = new GlideRecord('cmdb_ci_outage');
  	gr.addQuery('cmdb_ci', cmdb_ci);
  	gr.addQuery('type', 'outage')
  		.addOrCondition('type', 'planned');
  	gr.addQuery('begin', '<', end);
  	gr.addQuery('end', '>', start)
  		.addOrCondition('end', '=', 'NULL');
  	gr.orderBy('begin');
  	gr.query();

  	// get the beginning and end timespands for all outages
  	// found, so that overlapping outages can be collapsed later.
  	var beginMS;
  	var endMS;
  	while (gr.next()) {
  		var type = gr.getValue('type');
  		beginMS = gr.begin.getGlideObject().getNumericValue();
  		endMS = gr.end.getGlideObject().getNumericValue();
  		if (endMS == 0) { endMS = ed; } // if endMS ==0, then outage is ongoing. use end param for outage end value.
  		if (beginMS < sd) { beginMS = sd; }
  		if (endMS < beginMS) { continue; } // disregard outages of negative duration
  		if (endMS > ed) { endMS = ed; }
  		this._processOutage(outages, type, beginMS, endMS);
  	}

  	// QUERY FOR OUTAGES FROM MTOM TABLE
  	// new outage data will reference the cmdb_ci in the cmdb_outage_ci_mtom table
  	// and not necessarily directly on the cmdb_ci_outage table.
  	relatedOutages = new GlideRecord('cmdb_ci_outage');
  	relatedOutages.addQuery('cmdb_ci', '!=', cmdb_ci) // ADDED TO AVOID DUPLICATES
  		.addOrCondition('cmdb_ci', null); 

  	relatedOutages.addQuery('type', 'outage')
  		.addOrCondition('type', 'planned');
  	relatedOutages.addQuery('begin', '<', end);
  	relatedOutages.addQuery('end', '>', start)
  		.addOrCondition('end', '=', 'NULL');

  	var grJoin = relatedOutages.addJoinQuery('cmdb_outage_ci_mtom', 'sys_id', 'outage');
  	grJoin.addCondition('ci_item', cmdb_ci);

  	relatedOutages.orderBy('begin');
  	relatedOutages.query();

  	// get the beginning and end timespands for all related outages
  	// found, so that overlapping outages can be collapsed later.
  	while (relatedOutages.next()) {
  		var type = relatedOutages.getValue('type');
  		beginMS = relatedOutages.begin.getGlideObject().getNumericValue();
  		endMS = relatedOutages.end.getGlideObject().getNumericValue();
  		if (endMS == 0) { endMS = ed; }
  		if (endMS < beginMS) { continue; }
  		if (beginMS < sd) { beginMS = sd; }
  		if (endMS > ed) { endMS = ed; }
  		this._processOutage(outages, type, beginMS, endMS);
  	}
  	
  	return outages;
  },
  
  /**
   * A helper function used by _getOutages to process and consolidate the outage spans
   * This function differs from collapseOverlaps in that the outages are not required
   * to be ordered initially.
   *
   * Also, it is meant to be used to continuously collapse the outages as it iterates
   * through the results so that we can reduce heap memory utilization.
   *
   * @see DEF0083238 for more details
   * @param outages an array of outage span objects. Passed in and updated by reference.
   * @beginMS start time as a timestamp
   * @endMS end time as a timestamp
   * @return void
   */
  _processOutage : function(outages, type, beginMS, endMS) {
  	// IF LIST IS EMPTY THEN SIMPLY ADD OUTAGE
  	if (outages.length == 0) {
  		outages.push({start: beginMS, end: endMS, type: type});
  		return;
  	}
  	for (var i = 0; i < outages.length; i++) {
  		// 	IF CURRENT OUTAGE STARTS BEFORE AND END AFTER EXISTING OUTAGE
  		if (type != outages[i].type && beginMS < outages[i].start && endMS > outages[i].end) {
  			// PLANNED OUTAGE OVERWRITES EXISTING OUTAGE
  			if (type == 'planned') {
  				outages.splice(i, 1, {start: beginMS, end: endMS, type: type});
  				return;
  			}
  			// OUTAGE IS SPLIT BY EXISTING PLANNED OUTAGE
  			else if (type == 'outage') {
  				outages.splice((i+1), 0, {start: outages[i].end, end: endMS, type: type});
  				outages.splice(i, 0, {start: beginMS, end: outages[i].start, type: type});
  				return;
  			}
  		}
  		// IF CURRENT ENTIRELY BEFORE NEXT THEN ADD
  		if (endMS < outages[i].start) {
  			if (i == 0) {
  				outages.unshift({start: beginMS, end: endMS, type: type});
  			} else {
  				outages.splice(i, 0, {start: beginMS, end: endMS, type: type});
  			}
  			return;
  		}
  		// IF CURRENT COMPLETELY AFTER NEXT THEN SKIP UNLESS THIS IS ALREADY LAST ITEM IN THE LIST
  		if (beginMS > outages[i].end) {
  			if (i == (outages.length - 1)) {
  				outages.push({start: beginMS, end: endMS, type: type});
  				return;
  			} else {
  				continue;
  			}
  		}
  		// CHECK IF WE NEED TO EXTEND NEXT ENTRY IN EIHER DIRECTION
  		if ((beginMS < outages[i].start) || (endMS > outages[i].end)) {
  			if (beginMS < outages[i].start) {
  				// EXTEND BACKWARDS IF OUTAGE TYPE IS THE SAME
  				if (type == outages[i].type)
  					outages[i].start = beginMS;
  				else { // IF TYPE IS DIFFERENT
  					// IF CURRENT TYPE IS PLANNED, INSERT AND OVERWRITE EXISTING OUTAGE
  					if (type == 'planned') {
  						outages[i].start = endMS;
  						outages.splice((i), 0, {start: beginMS, end: endMS, type: type});
  					} else {
  						// INSERT CURRENT OUTAGE BEFORE, OVERWRITE CURRENT OUTAGE WITH EXISTING PLANNED OUTAGE TIME
  						outages.splice((i-1), 0, {start: beginMS, end: outages[i].start, type: type});
  					}
  				}
  				// NEED COUNTER TO PROCESS OUTAGES BEFORE CURRENT
  				var prev = (i - 1);
  				while (prev >= 0) {
  					// NEED TO HANDLE PREVIOUS OUTAGES IF THEY OVERLAPS WITH THE CURRENT OUTAGE
  					// CONTINUE TO ITERATE THROUGH PREVIOUS OUTAGES AS LONG AS THERE IS OVERLAP
  					if (outages[prev].type == 'outage') {
  						if ((outages[prev].start >= outages[i].start) && (outages[prev].end <= outages[i].end)) {
  							// CHECK FOR PREVIOUS ENTRIES WE CAN REMOVE
  							outages.splice(prev, 1);
  							prev--;
  						} else if (outages[prev].end > outages[i].start) {
  							// ADJUST PREVIOUS ADJACENT ENTRY END DATE IF START OVERLAPS
  							outages[prev].end = outages[i].start;
  							prev--;
  						} else {
  							break;
  						}
  					} else {
  						break;
  					}
  				}

  				// MERGE ANY PREVIOUS OUTAGES THAT OVERLAP
  				prev = (i - 1);	// reset previous
  				this._mergeBefore(prev, outages);
  			} else if (endMS > outages[i].end) {
  				// EXTEND FORWARDS IF OUTAGE TYPE IS THE SAME
  				if (type == outages[i].type)
  					outages[i].end = endMS;
  				else { // IF TYPE IS DIFFERNT
  					// IF CURRENT TYPE IS PLANNED, INSERT AND OVERWRITE EXISTING OUTAGE
  					if (type == 'planned') {
  						outages.splice((i+1), 0, {start: beginMS, end: endMS, type: type});
  						outages[i].end = beginMS;
  					} else {
  						// INSERT CURRENT OUTAGE AFTER, OVERWRITE CURRENT OUTAGE WITH EXISTING PLANNED OUTAGE TIME
  						outages.splice((i+1), 0, {start: outages[i].end, end: endMS, type: type});
  					}
  				}
  				// NEED TO HAVE COUNTER TO PROCESS OUTAGES AFTER CURRENT
  				var next = (i + 1);
  				while (next < outages.length) {
  					// NEED TO HANDLE NEXT OUTAGES IF THEY OVERLAPS WITH THE CURRENT OUTAGE
  					// CONTINUE TO ITERATE THROUGH NEXT OUTAGES AS LONG AS THERE IS OVERLAP
  					if (outages[next].type == 'outage') {
  						if ((outages[next].start >= outages[i].start) && (outages[next].end <= outages[i].end)) {
  							// CHECK FOR FUTURE ENTRIES WE CAN REMOVE
  							outages.splice(next, 1);
  							next++;
  						} else if (outages[next].start < outages[i].end) {
  							// ADJUST NEXT ADJACENT ENTRY START DATE IF END OVERLAPS
  							outages[next].start = outages[i].end;
  							next++;
  						} else {
  							break;
  						}
  					} else {
  						break;
  					}
  				}

  				// MERGE ANY SUBSEQUENT OUTAGES THAT OVERLAP
  				next = (i + 1); // reset next
  				this._mergeAfter(next, outages);
  			}
  			return;
  		}
  		// IF CURRENT PLANNED OUTAGE IS COMPLETELY DURING AN EXISTING OUTAGE 
  		if (type == 'planned' && outages[i].type == 'outage' && beginMS >= outages[i].start && endMS <= outages[i].end) {
  			// INSERT PLANNED OUTAGE AFTER EXSTING OUTAGE
  			outages.splice((i+1), 0, {start: beginMS, end: endMS, type: type});
  			// SPLIT EXISTING OUTAGE IN SECOND OUTAGE THAT BEGINS AFTER CURRENT PLANNED OUTAGE
  			if (endMS != outages[i].end)
  				outages.splice((i+2), 0, {start: endMS, end: outages[i].end, type: outages[i].type});
  			// EXISTING OUTAGE END WHERE PLANNED OUTAGE BEGINS
  			outages[i].end = beginMS;
  			// REMOVE OUTAGES WITH SAME START/END	
  			if (outages[i].end == outages[i].start)
  				outages.splice(i,1);
  		}
  		// IF WE FALL THROUGH TO HERE THEN NO ACTION REQUIRED
  		return;
  	}
  },

  /**
   * Merge subsequent outages of the same type together if they overlap 
   * @param {Number} index 
   * @param {Array<Object>} outages 
   * @returns number 
   */
  _mergeAfter: function (index, outages) {
  	if ((typeof index != 'number') || !outages || !outages.length)
  		return -1;

  	var current = index;
  	while (current < outages.length) {
  		var next = (current + 1);
  		if (next < outages.length && (outages[current].end > outages[next].start) && (outages[current].type == outages[next].type)) {
  			outages[current].end = Math.max(outages[current].end, outages[next].end);
  			outages.splice(next, 1);
  			current++;
  		} else
  			return current;
  	}
  	return current;
  },

  /**
   * Merge previous outages of the same type together if they overlap 
   * @param {Number} index 
   * @param {Array<Object>} outages 
   * @returns Number 
   */
  _mergeBefore: function (index, outages) {
  	if ((typeof index != 'number') || !outages || !outages.length)
  		return -1;

  	var current = index;
  	while (current >= 0) {
  		var prev = (current - 1);
  		if (prev >= 0 && (outages[current].start < outages[prev].end) && (outages[current].type == outages[prev].type)) {
  			outages[current].start = Math.min(outages[current].start, outages[prev].start);
  			outages.splice(prev, 1);
  			current--;
  		} else
  			return current;
  	}
  	return current;
  },
  	
  _getFullDay: function(tz) {
      var answer = new GlideSchedule();
      var gr = new GlideRecord('cmn_schedule_span');
      gr.initialize(); // necessary to force in a guid
      gr.setValue('start_date_time', '20100101T000000');
      gr.setValue('end_date_time', '20100102T000000');
      gr.setValue('repeat_type', 'daily');
      answer.addTimeSpan(gr); // gr must have a guid at this point
  	if (tz)
  		answer.setTimeZone(tz);
      return answer;
  },

  _collapseOverlaps: function(outages) {
      var answer = new Array();
      var work = null;
      for (var i = 0; i < outages.length; i++) {
          var o = outages[i];
          if (work == null) {
              work = o;
              continue;
          }
          // overlap
          if (o.start <= work.end)
              work.end = Math.max(o.end, work.end);
          else {
              answer.push(work);
              work = o;
          }
      }
      if (work)
          answer.push(work);

      return answer;
  },

  setCommitment: function(id) {
      this.commitment = id;
  },

  setCI: function(cmdb_ci) {
      this.cmdb_ci = cmdb_ci;
  }

};

Sys ID

f68ee4d70a0a0bb900bdb7319b857ce3

Offical Documentation

Official Docs: