lib/services/obm-service.js
// Copyright 2015, EMC, Inc.
'use strict';
var di = require('di');
module.exports = obmServiceFactory;
di.annotate(obmServiceFactory, new di.Provide('Task.Services.OBM'));
di.annotate(obmServiceFactory, new di.Inject(
'Promise',
'_',
di.Injector,
'Assert',
'Logger',
'Services.Configuration',
'Services.Lookup',
'Constants',
'Services.Encryption',
'Services.Waterline',
'Errors'
));
function obmServiceFactory(
Promise,
_,
injector,
assert,
Logger,
config,
lookup,
Constants,
encryption,
waterline,
Errors
) {
var logger = Logger.initialize(obmServiceFactory);
function lookupHost (options) {
if (options.config.host && Constants.Regex.MacAddress.test(options.config.host)) {
return lookup.macAddressToIp(options.config.host).then(function (ipAddress) {
options.config.host = ipAddress;
return options;
});
}
return options;
}
/**
* An OBM command interface that runs raw OBM commands from
* various OBM services with failure and retry logic.
*
* @constructor
*/
function ObmService(nodeId, obmServiceFactory, obmSettings, options) {
assert.object(obmSettings);
assert.object(obmSettings.config);
assert.string(obmSettings.service);
assert.func(obmServiceFactory);
assert.isMongoId(nodeId);
this.params = options || {};
this.retries = this.params.retries;
this.delay = this.params.delay;
if (this.params.delay !== 0) {
this.delay = this.params.delay || config.get('obmInitialDelay') || 500;
}
if (this.params.retries !== 0) {
this.retries = this.params.retries || config.get('obmRetries') || 6;
}
this.serviceFactory = obmServiceFactory;
this.obmConfig = obmSettings.config;
this.serviceType = obmSettings.service;
this.nodeId = nodeId;
}
ObmService.prototype.kill = function() {
// This will propagate down to the child process object running the
// command, which in turn will reject an error that gets propagated up
// and caught by _runObmCommand
if (this.service && _.isFunction(this.service.kill)) {
this.service.kill();
}
};
/* An obmSetting object will look something like below. It
* will be an array of OBM configurations which contain a DI
* service name, and necessary configuration data:
* [
* {
* service: 'ipmi-obm-service',
* config: {
* 'user': 'ADMIN',
* 'password': 'ADMIN',
* 'host': '192.168.100.12'
* }
* },
* {
* service: 'vbox-obm-service',
* config: {
* 'alias': 'test-vm',
* 'user': 'renasar'
* }
* }
* ]
*/
/**
* Iterates through an array of OBM configurations and instantiates
* OBM service objects at runtime. On an OBM call failure it attemps
* to try the next OBM service configuration in the array.
*
* @function _runObmCommand
*
* @param {Object[]} settings - array of OBM configuration objects
* @param {String} obmCommand - function interface name to call
* @param {String} nodeId - identifier for the node
* @param {boolean} [childProcessFailure] - a stateful variable that
* keeps track of whether any of our failures have been child process
* related, or merely configuration/interface related. If no errors were
* child process related, then we shouldn't retry the command.
*
* @returns {Promise}
*/
ObmService.prototype.runObmCommand = function (obmCommand) {
var self = this;
return Promise.resolve({
delay: this.delay,
retries: this.retries,
config: this.obmConfig,
params: this.params,
nodeId: this.nodeId
})
.then(lookupHost)
.then(function (options) {
self.service = self.serviceFactory.create(options);
return self.service[obmCommand](self.nodeId);
});
};
/**
* Attempts to run an OBM command a specified number of times until
* success.
*
* @function _retryObmCommand
*
* @param {number} retryCount - Recursive counter for runObmCommand retry count
* @param {String} nodeId - The identifier for the node
* @param {String} obmCommand - an interface function name
* @param {*} expected - optional expected output to determine
* success or not. Used primarily with powerStatus assertions.
* @param {number} delay - amount of time (in ms) to delay in between retries
* @param {number} retries - number of attempts to make
*
* @returns {Promise}
*/
ObmService.prototype._retryObmCommand = function(retryCount, obmCommand, expected, delay) {
var self = this;
return self.runObmCommand(obmCommand)
.then(function(out) {
if (expected !== undefined && out !== expected) {
logger.warning("Expected power state " + expected +
" does not match actual state " + out +
". Retrying power status, attempt " + retryCount + "...",
{id: self.nodeId});
if (retryCount < self.retries) {
logger.debug("Retrying ObmService command.", {
id: self.nodeId,
obmCommand: obmCommand
});
return Promise.delay(delay)
.then(function() {
return self._retryObmCommand(
retryCount + 1, obmCommand, expected, delay * 2);
});
} else {
throw new Error("Exceeded maximum retries for obmCommand to " +
"have expected output " +
"(expected: " + expected + ", " +
"actual: " + out + " ).");
}
} else {
logger.info("OBM command (" + obmCommand + ") success. Type: " +
self.serviceType, { id: self.nodeId });
return out;
}
});
};
/**
* Attempts to run an OBM command a specified number of times until
* success.
*
* @function retryObmCommand
*
* @param {Object} options options for the retry
* @param {String} options.nodeId - The identifier for the node
* @param {String} options.obmCommand - an interface function name
* @param {number} [options.delay] - amount of time (in ms) to delay in between retries
* @param {number} [options.retries] - number of attempts to make
* @param {*} [options.expected] - optional expected output to determine
* success or not. Used primarily with powerStatus assertions.
*
* @returns {Promise}
*/
ObmService.prototype.retryObmCommand = function (obmCommand, expected) {
var self = this;
assert.string(obmCommand);
return self._retryObmCommand(0, obmCommand,
expected, self.delay)
.catch(function(error) {
logger.warning("runObmCommand error.", {
error: error,
id: self.nodeId,
command: obmCommand
});
// Not all services support this, and that's okay, don't fail
// a common reboot/pxe boot use case for machines using these obm
// services, as they should ideally be configured to pxe already.
if (error.name === 'ObmConfigurationError' && obmCommand === 'setBootPxe') {
return;
} else {
throw error;
}
});
};
/**
* Takes an OBM on/off command and wraps it with status checks to
* prevent idempotency failures from some OBM services, and to assert
* that our power calls have actually happened.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId - The identifier for the node
* @param {String} obmCommand - an interface function name
*
* @returns {Promise} - returns a promise fulfilled with the value of
* the power status.
*/
ObmService.prototype.wrapWithStatusCheck = function (obmCommand) {
var self = this;
logger.debug("Invoking wrapWithStatusCheck "+self.nodeId+" "+obmCommand);
// Decide whether we have to run the command or not
return self.powerStatus()
.then(function (on) {
if ((on && obmCommand === 'powerOn') ||
(!on && obmCommand === 'powerOff') ||
(!on && obmCommand === 'soft' )) {
// on here is either a true or false value.
return Promise.resolve(on);
} else {
// If we're not already in the desired state, run the command
return self.retryObmCommand(obmCommand)
.then(function () {
// Assert we've affected the power state.
if (obmCommand === 'powerOn') {
// return self.powerStatus(self.nodeId, true);
//
// Don't bother asserting power status after turning
// it on. Sometimes it takes too long (with AMT
// specifically) therefore causing race conditions
// with workflow states and
// properties.request template rendering.
// We can ascertain this information anyways when
// the node fails to come up.
return Promise.resolve(true);
} else if (obmCommand === 'powerOff' || obmCommand === 'soft') {
// Give a little breathing room after the powerOff
// call before we assert the status.
return Promise.delay(500)
.then(function () {
logger.info("Asserting power status is off. ", {
id: self.nodeId
});
return self.powerStatus(false);
});
} else {
logger.deprecate("ObmService.wrapWithStatusCheck should only be " +
"used with powerOn and powerOff/soft commands.");
return Promise.resolve();
}
});
}
});
};
/**
* MC Reset Cold interface.
*
* Performs an mc reset cold on machine. IPMI specific.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.mcResetCold = function() {
var self = this;
return self.retryObmCommand('mcResetCold')
.then(function() {
// BMCs take a while to reset
return Promise.delay(50 * 1000);
})
.then(function() {
return self.retryObmCommand('mcInfo');
});
};
/**
* Power On interface.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.powerOn = function() {
return this.wrapWithStatusCheck('powerOn');
};
/**
* Power Off interface.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.powerOff = function() {
return this.wrapWithStatusCheck('powerOff');
};
/**
* Soft Power Off interface. IPMI specific.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.softPowerOff = function() {
return this.wrapWithStatusCheck('soft');
};
/**
* Reboot interface.
*
* Overrides typical reboot functionality with a safer sequence
* consisting of status checks, powering off, and powering on.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.reboot = function() {
var self = this;
return self.powerOff()
.then(function() {
return self.powerOn();
});
};
/**
* Hard reset interface.
*
* Performs a hard reset of machine. IPMI specific.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.reset = function() {
return this.retryObmCommand('reset');
};
/**
* Soft Reset interface.
*
* Performs a soft reset of machine. IPMI specific.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.softReset = function() {
var self = this;
return self.softPowerOff()
.then(function() {
return self.powerOn();
});
};
/**
* Power Status interface.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @param {boolean} [expected] on/off value used on internal calls to powerStatus
* to assert that powerOn or powerOff calls have succeeded.
*
* @returns {Promise}
*/
ObmService.prototype.powerStatus = function(expected) {
return this.retryObmCommand('powerStatus', expected);
};
/**
* Set the node to PXE boot on next boot.
* Not all OBM services will implement this function.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.setBootPxe = function() {
// Delay less here since some OBM services
// just won't have this call.
return this.retryObmCommand('setBootPxe');
};
/**
* Force the node to PXE boot on next boot without clearing PXE bootflag by 60s timeout or PEF
* reset. Not all OBM services will implement this function.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.forceBootPxe = function() {
// Delay less here since some OBM services
// just won't have this call.
return this.retryObmCommand('forceBootPxe');
};
/**
* Clear the boot up watchdog to make those servers not reboot after entering OS.
* Not all OBM services will implement this function.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.clearWatchDog = function() {
return this.retryObmCommand('clearWatchDog');
};
/**
* Set the node to Disk boot on next boot. Not all OBM services will
* implement this function.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.setBootDisk = function() {
// Delay less here since some OBM services
// just won't have this call.
return this.retryObmCommand('setBootDisk');
};
/**
* Enable the identify light (on) for the node. Not all OBM services
* will implement this function.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.identifyOn = function() {
return this.retryObmCommand('identifyOn');
};
/**
* Disable the identify light (off) for the node. Not all OBM services
* will implement this function.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.identifyOff = function() {
return this.retryObmCommand('identifyOff');
};
/**
* Blink the identify light (blink) for the node. Not all OBM services
* will implement this function.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.identifyBlink = function() {
return this.retryObmCommand('identifyBlink');
};
/**
* Will clear the System Event Log. Not all OBM services
* will implement this function.
*
* @memberOf ObmService
* @function
*
* @param {String} nodeId
* @returns {Promise}
*/
ObmService.prototype.clearSEL = function() {
return this.retryObmCommand('clearSEL');
};
/**
* Will send NMI (non-maskable interrupt) to host
* will implement this function.
*
* @memberOf ObmService
* @function
*
* @returns {Promise}
*/
ObmService.prototype.NMI = function() {
return this.retryObmCommand('NMI');
};
/**
* Will send command to 'push' the sysyem power button
* will implement this function.
*
* @memberOf ObmService
* @function
*
* @returns {Promise}
*/
ObmService.prototype.powerButton = function() {
return this.retryObmCommand('powerButton');
};
/**
* Check whether the specified node support obmSettings
*
* @memberOf ObmService
* @function
*
* @description
* The supported list in on-core/lib/common/constants.js looks like:
* ObmSettings: {
* 'panduit-obm-service': [
* {
* type: 'enclosure'
* },
* {
* type: 'compute',
* sku: 'Megatron'
* }
* ]
* }
*
* @param {Object} node
* @param {Object} obmSettings
* @return {String} invalidService - Unsupported service if any
*/
ObmService.checkValidService = function(node, obmSettings) {
var reqService;
obmSettings = _.isArray(obmSettings) ? obmSettings : [obmSettings];
// Check every obm setting
// Only resolved when all settings are supported
return Promise.map(obmSettings, function(setting) {
reqService = setting.service;
// If service is not listed, assume it is supported in current node
if (!Constants.ObmSettings[reqService]) {
return;
}
// Check rules in one obm setting support list
// Resolved when any of these rules are matched
return Promise.any(_.map(Constants.ObmSettings[reqService], function(rule) {
// Check items in one rule
// Resolved when all items are fulfilled
return Promise.reduce(_.keys(rule), function(ret, key) {
return Promise.resolve().then(function () {
// Process special rules before comparing
if (key === 'sku') {
return _getValidSku(node);
}
})
.then(function() {
if (node[key] && (rule[key] !== node[key])) {
return Promise.reject();
}
else {
return ret;
}
})
.catch(function() {
ret = false;
return ret;
});
}, true)
.then(function(result) {
if (result === true) {
return Promise.resolve();
}
else {
return Promise.reject();
}
});
})).catch(function() {
throw new Errors.BadRequestError(
'Service ' + reqService + ' is not supported in current node'
);
});
});
};
/**
* Get valid sku info from database
*/
function _getValidSku (node) {
if (node.sku) {
return waterline.skus.findOne({ id: node.sku }).then(function (sku) {
if (sku && sku.name) {
node.sku = sku.name;
}
else {
return Promise.reject();
}
});
}
else {
return Promise.reject();
}
}
ObmService.create = function(nodeId, obmServiceFactory, obmSettings, options) {
return new ObmService(nodeId, obmServiceFactory, obmSettings, options);
};
return ObmService;
}