lib/services/nodes-api-service.js
// Copyright © 2017 Dell Inc. or its subsidiaries. All Rights Reserved.
'use strict';
var di = require('di');
module.exports = nodeApiServiceFactory;
di.annotate(nodeApiServiceFactory, new di.Provide('Http.Services.Api.Nodes'));
di.annotate(nodeApiServiceFactory,
new di.Inject(
'Http.Services.Api.Workflows',
'Services.Waterline',
'Errors',
'Logger',
'_',
'Promise',
'Constants',
'Task.Services.OBM',
'Services.Configuration',
'ipmi-obm-service',
'Assert',
'Protocol.Events'
)
);
function nodeApiServiceFactory(
workflowApiService,
waterline,
Errors,
Logger,
_,
Promise,
Constants,
ObmService,
configuration,
ipmiObmServiceFactory,
assert,
eventsProtocol
) {
var logger = Logger.initialize(nodeApiServiceFactory);
function NodeApiService() {
}
/**
* Find target nodes that relate with the removing node
* @param {Object} relations - The relations object of a node
* @param {String} type
* @return {Promise} array of target nodes
*/
NodeApiService.prototype._findTargetNodes = function(relations, type) {
return this._needTargetNodes(relations, type)
.catch(function (err) {
logger.warning("Error getting target node with type " + type,
{ error: err });
return [];
});
};
NodeApiService.prototype._needTargetNodes = function(relations, type) {
if (!relations) {
return Promise.resolve([]);
}
var relation = _.find(relations, { relationType: type });
if (!relation || !_.has(relation, 'targets') ) {
return Promise.resolve([]);
}
return Promise.map(relation.targets, function (targetNodeId) {
return waterline.nodes.needByIdentifier(targetNodeId);
});
};
/**
* Remove targets from node relations. If the node is invalid
* or doesn't have required relation, this function doesn't need to update the
* node info and ignore silently with Promise.resolve().
* If the targets list of one relation item is empty after removing, delete this relations.
* @param {Object} node node whose relation needs to be updated
* @param {String} type relation type that needs to be updated
* @param {String[] | Object[]} targets - nodes or ids in the relation
* that needs to be deleted
* @return {Object} node after removing relation
*/
NodeApiService.prototype.removeRelation = function removeRelation(node, type, targets) {
var self = this;
if (!node || !type || !_.has(node, 'relations')) {
return;
}
var index = _.findIndex(node.relations, { relationType: type });
if (index === -1 || !_.has(node.relations[index], 'targets')) {
return;
}
// Remove relations with blank targets
if (node.relations[index].targets.length === 0) {
var relationsToBeRemoved = {"relations": [node.relations[index]]};
return waterline.nodes.removeListItemsByIdentifier(node.id, relationsToBeRemoved);
}
if (!targets){
return Promise.resolve(node);
}
// Remove target node id in relation field
targets = [].concat(targets).map(function(node) {return node.id || node;});
var field = ["relations.", String(index),".targets"].join("");
var targetsToBeRemoved = {};
targetsToBeRemoved[field] = targets;
return waterline.nodes.removeListItemsByIdentifier(node.id, targetsToBeRemoved)
.then(function(modifiedNode){
return self.removeRelation(modifiedNode, type);
});
};
/**
* Add the given target nodes to the given relationType on the given node. Fail
* silently with missing arguments. If a relation does not already exist on the node
* create it, otherwise append to the existing one.
* @param {Object} node - node whose relation needs to be updated
* @param {String} type - relation type that needs to be updated
* @param {String[] | Object[]} targets - nodes or ids in relation type that needs to be added
* @return {Object} the updated node
*/
NodeApiService.prototype.addRelation = function addRelation(node, type, targets) {
if (!(node && type && targets)) {
return;
}
return waterline.nodes.addFieldIfNotExistByIdentifier(node.id, "relations", [])
.then(function(){
var relationsItemToBeAdded = {
relations: [{relationType: type, targets: []}]
};
var existSign = [{relationType: type}];
return waterline.nodes.addListItemsIfNotExistByIdentifier(
node.id,
relationsItemToBeAdded,
existSign
);
})
.then(function(modifiedNode){
if (!modifiedNode){
return node;
}
return modifiedNode;
})
.then(function(modifiedNode){
var selfRefd = false;
var targetsItems = _.map([].concat(targets), function(targetNode) {
if(targetNode === node.id ) {
selfRefd = true;
} else {
return targetNode;
}
});
if (selfRefd) {
return Promise.reject(
new Error('Node cannot have relationship '+type+' with itself'));
}
var index = _.findIndex(modifiedNode.relations, { relationType: type });
var field = ["relations.", String(index),".targets"].join("");
var targetsToBeAdded = {};
targetsToBeAdded[field] = _.uniq(targetsItems);
// Can not make sure prevent every exception in high concurrency.
if (type === 'containedBy' &&
modifiedNode.relations[index].targets.length + targets.length > 1) {
return Promise.reject(
new Error("Node "+node.id+" can only be contained by one node"));
}
// Compute node can only have one enclosure target.
if (type === "enclosedBy") {
var targetsToBeRemoveded = {};
targetsToBeRemoveded[field] = [modifiedNode.relations[index].targets[0]];
return waterline.nodes.removeListItemsByIdentifier(
node.id, targetsToBeRemoveded
)
.then(function(){
return targetsToBeAdded;
});
}
return targetsToBeAdded;
})
.then(function(targetsToBeAdded){
return waterline.nodes.addListItemsIfNotExistByIdentifier(
node.id, targetsToBeAdded
);
});
};
/**
* Check whether a node is valid to be deleted
* @param {String} nodeId
* @return {Promise}
*/
NodeApiService.prototype._delValidityCheck = function(nodeId) {
return workflowApiService.findActiveGraphForTarget(nodeId)
.then(function (graph) {
if (graph) {
// If there is active workflow, the node cannot be deleted
return Promise.reject('Could not remove node ' + nodeId +
', active workflow is running');
}
return Promise.resolve();
});
};
/**
* Remove node related data and remove its relations with other nodes
* @param {Object} node
* @param {String} srcType
* @return {Promise}
*/
NodeApiService.prototype.removeNode = function(node, srcType) {
var self = this;
return self._delValidityCheck(node.id)
.then(function () {
if (!node.hasOwnProperty('relations')) {
return Promise.resolve();
}
return Promise.map(node.relations, function(relation) {
var type = relation.relationType;
// Skip handling relationType that comes from the upstream node
// to avoid deleting upstream nodes more than once
if (srcType && (srcType === type)) {
return Promise.resolve();
}
if (!Constants.NodeRelations[type]) {
return Promise.resolve();
}
// Otherwise update targets node in its "relationType"
return self._findTargetNodes(node.relations, type)
.then(function(targetNodes) {
if(Constants.NodeRelations[type].relationClass === 'component' &&
type.indexOf('By') === -1) {
return Promise.map(targetNodes, function(targetNode) {
return self._delValidityCheck(targetNode.id);
}).then(function() {
return Promise.map(targetNodes, function(targetNode) {
return self.removeNode(
targetNode, Constants.NodeRelations[type].mapping
);
});
});
} else {
return Promise.map(targetNodes, function(targetNode) {
return self.removeRelation(
targetNode,
Constants.NodeRelations[type].mapping,
node.id
);
});
}
});
});
})
.then(function () {
return Promise.settle([
//lookups should be destoryed here, only clear node field
//as a workaround until the issue that when lookups are cleared
//it cannot be updated timely in nodes' next bootup is fixed
waterline.lookups.update({ node: node.id },{ node: '' } ),
waterline.nodes.destroy({ id: node.id }),
waterline.catalogs.destroy({ node: node.id }),
waterline.workitems.destroy({ node: node.id })
]);
})
.then(function () {
return eventsProtocol.publishNodeEvent(node, 'removed');
})
.then(function () {
logger.debug('node deleted', {id: node.id, type: node.type});
return node;
});
};
/**
* Get list of nodes
* @param {Object} query [req.query] HTTP Request
* @returns {Promise}
*/
NodeApiService.prototype.getAllNodes = function(query, options) {
options = options || {};
return Promise.try(function() {
query = waterline.nodes.find(query);
if (options.skip) {
query.skip(options.skip);
}
if (options.limit) {
query.limit(options.limit);
}
return query.populate('obms').populate('ibms');
});
};
NodeApiService.prototype.postNode = function(body) {
var nodeBody = _.omit(body, ['obms', 'ibms']);
var obmBody = body.obms || body.obmSettings || null;
var ibmBody = body.ibms || null;
return Promise.resolve()
.then(function() {
return waterline.nodes.create(nodeBody);
}).tap(function(node) {
return eventsProtocol.publishNodeEvent(node, 'added');
}).tap(function(node) {
if (obmBody) {
return Promise.map(obmBody, function(obm) {
return waterline.obms.upsertByNode(node.id, obm);
});
}
}).tap(function(node) {
if (ibmBody) {
return Promise.map(ibmBody, function(ibm) {
return waterline.ibms.upsertByNode(node.id, ibm);
});
}
}).then(function(node) {
return [node, waterline.ibms.findByNode(node.id, 'snmp-ibm-service')];
}).spread(function(node, snmpSettings) {
if(node.type === Constants.NodeTypes.Switch &&
snmpSettings && node.autoDiscover) {
return workflowApiService.createAndRunGraph(
{
name: 'Graph.Switch.Discovery',
options: {
defaults: _.assign(snmpSettings, { nodeId: node.id })
}
},
node.id
);
}
else if(node.type === Constants.NodeTypes.Pdu &&
snmpSettings && node.autoDiscover) {
return workflowApiService.createAndRunGraph(
{
name: 'Graph.PDU.Discovery',
options: {
defaults: _.assign(snmpSettings, { nodeId: node.id })
}
},
node.id
);
}
else if(node.type === Constants.NodeTypes.Mgmt &&
obmBody && node.autoDiscover) {
var configuration = {
name: 'Graph.MgmtSKU.Discovery',
options: {
defaults: {
graphOptions: {
target: node.id
},
nodeId: node.id
}
}
};
return workflowApiService.createAndRunGraph(configuration);
}
return node;
});
};
NodeApiService.prototype.getNodeById = function(id) {
return waterline.nodes.getNodeById(id)
.then(function (node){
if (!node) {
throw new Errors.NotFoundError(
'Node not Found ' + id
);
}
return node;
});
};
NodeApiService.prototype.getNodeRelations = function(id) {
return waterline.nodes.needByIdentifier(id)
.then(function (node){
return node.relations || [];
});
};
/**
* Edit the relations of a given node, delegating object manipulations to the given handler.
* Handle the update with the handler's output
*
* @param {String} id - a node id
* @param {Object} body - an object with relation types as keys and arrays of target
* node ids as values.
* @param {Function} handler - a function(node, relationType, targets) which edits the
* given node's relations and updates the node
*/
NodeApiService.prototype.editNodeRelations = function(id, body, handler) {
var self = this;
return waterline.nodes.needByIdentifier(id).bind({})
.then(function(node) {
this.parentNode = node;
return Promise.all(_.transform(body, function(result, targets, relationType) {
result.push(self._needTargetNodes(
[{relationType: relationType, targets:targets}],
relationType
)
);
result.push(relationType);
}, []));
})
.then(function(targetRelations) {
var parentNode = this.parentNode;
targetRelations = _.chunk(targetRelations, 2); //divide the array into subArray chunks
//of [[targetNodes], relationType]
return Promise.all(_.compact(_.map(targetRelations, function(relationSet){
var targetNodes = relationSet[0];
var relationType = relationSet[1];
if (!Constants.NodeRelations[relationType]) {
return;
}
return handler.call(
self,
parentNode,
relationType,
targetNodes
);
})));
});
};
NodeApiService.prototype.patchNodeById = function(id, body) {
return waterline.nodes.needByIdentifier(id)
.then(function () {
return waterline.nodes.updateByIdentifier (
id,
body
);
});
};
NodeApiService.prototype.delNodeById = function(id) {
var self = this;
return waterline.nodes.needByIdentifier(id)
.then(function (node) {
return self.removeNode(node);
});
};
NodeApiService.prototype.getNodeObmById = function(id) {
return waterline.nodes.needByIdentifier(id);
};
NodeApiService.prototype.postNodeObmIdById = function(id, body) {
// TODO: Make this a taskGraph instead once we improve multiple task
// graph handling per node.
return waterline.obms.findByNode(id, 'ipmi-obm-service', true)
.then(function (settings) {
if (settings) {
var obmService = ObmService.create(id, ipmiObmServiceFactory, settings);
if (body && body.value) {
return obmService.identifyOn(id);
} else {
return obmService.identifyOff(id);
}
} else {
throw new Errors.NotFoundError(
'No IPMI OBM Settings Found (' + id + ').'
);
}
});
};
NodeApiService.prototype.getNodeSshById = function(id) {
return waterline.nodes.getNodeById(id)
.then(function (node) {
if (node) {
return waterline.ibms.findAllByNode(id, false, {service: 'ssh-ibm-service'});
}
});
};
/**
* Create an OBM for the specified Node id
* @param {String} id
* @param {Object} obm
* @return {Promise}
*/
NodeApiService.prototype.postNodeSshById = function(id, ibm) {
return waterline.nodes.getNodeById(id)
.then(function (node) {
if (!node) {
throw new Errors.NotFoundError(
'Node not Found ' + id
);
}
return waterline.ibms.upsertByNode(id, ibm);
});
};
NodeApiService.prototype.getNodeCatalogById = function(id, query) {
return waterline.nodes.needByIdentifier(id)
.then(function (node) {
query = _.merge({ node: node.id }, query);
return waterline.catalogs.find(query);
});
};
NodeApiService.prototype.getNodeCatalogSourceById = function(id, source) {
return waterline.nodes.needByIdentifier(id)
.then(function(node) {
if (node && node.id) {
return waterline.catalogs.findLatestCatalogOfSource(
node.id, source
).then(function (catalogs) {
if (_.isEmpty(catalogs)) {
throw new Errors.NotFoundError(
'No Catalogs Found for Source (' + source + ').'
);
}
return catalogs;
});
}
});
};
NodeApiService.prototype.getPollersByNodeId = function (id) {
return waterline.nodes.needByIdentifier(id)
.then(function (node) {
if (node) {
return waterline.workitems.findPollers({ node: node.id });
}
});
};
NodeApiService.prototype.addToDhcpWhitelist = function (macAddr) {
// TODO: add this to DHCP protocol and send over that exchange
var whitelist = configuration.get('whitelist') || [];
whitelist.push(macAddr.replace(/:/g, '-'));
configuration.set('whitelist', whitelist);
return whitelist;
};
NodeApiService.prototype.delFromDhcpWhitelist = function (macAddr) {
// TODO: add this to DHCP protocol and send over that exchange
var whitelist = configuration.get('whitelist');
if (!_.isEmpty(whitelist)) {
_.remove(whitelist, function(mac) {
return mac === macAddr.replace(/:/g, '-');
});
configuration.set('whitelist', whitelist);
}
};
NodeApiService.prototype.getNodeWorkflowById = function (id, query) {
return waterline.nodes.needByIdentifier(id)
.then(function () {
return workflowApiService.getWorkflowsByNodeId(id, query);
});
};
NodeApiService.prototype.setNodeWorkflow = function (configuration, id) {
return workflowApiService.createAndRunGraph(configuration, id);
};
NodeApiService.prototype.setNodeWorkflowById = function(configuration, id) {
return workflowApiService.createAndRunGraph(configuration, id);
};
NodeApiService.prototype.getActiveNodeWorkflowById = function(id) {
return waterline.nodes.needByIdentifier(id)
.then(function (node) {
return workflowApiService.findActiveGraphForTarget(node.id);
});
};
NodeApiService.prototype.delActiveWorkflowById = function (id) {
return waterline.nodes.needByIdentifier(id)
.then(function (node) {
return workflowApiService.findActiveGraphForTarget(node.id);
})
.then(function(graph) {
if (_.isEmpty(graph)) {
throw new Errors.NotFoundError(
'No active workflow graph found for node ' + id
);
}
console.log(graph);
return [graph, workflowApiService.cancelTaskGraph(graph.instanceId)];
})
.spread(function(graph, graphId) {
if (!graphId) {
throw new Errors.NotFoundError(
'No active workflow instance ' +
graph.instanceId + ' found for node ' + id
);
} else {
return graph;
}
});
};
/**
* Add the tags to the specified node
* @param {String} id
* @param {Array} tags
* @return {Promise}
*/
NodeApiService.prototype.addTagsById = function(id, tags) {
return Promise.resolve().then(function() {
assert.ok(Array.isArray(tags), 'tags must be an array');
assert.isMongoId(id, 'the id must be a valid mongo id');
})
.then(function() {
return waterline.nodes.needByIdentifier(id);
})
.then(function () {
return waterline.nodes.addTags(id, tags);
})
.then(function() {
return tags;
});
};
/**
* Remove the tag from the specified node
* @param {String} id
* @param {String} tagName
* @return {Promise}
*/
NodeApiService.prototype.removeTagsById = function(id, tagName) {
return Promise.resolve().then(function() {
assert.string(tagName, 'tag must be a string');
assert.isMongoId(id, 'the id must be a valid mongo id');
})
.then(function() {
return waterline.nodes.needByIdentifier(id);
})
.then(function () {
return waterline.nodes.remTags(id, tagName);
})
.then(function() {
return tagName;
});
};
/**
* Remove the tag from a list of nodes
* @param {String} tagName
* @return {Promise}
*/
NodeApiService.prototype.masterDelTagById = function(tagName) {
return Promise.resolve().then(function() {
assert.string(tagName, 'tag must be a string');
})
.then(function() {
return waterline.nodes.findByTag(tagName);
})
.then(function(nodes) {
if(_.isEmpty(nodes)) {
throw new Errors.NotFoundError('name ' + tagName + ' was not found in nodes');
}
return nodes;
})
.map(function(node) {
return waterline.nodes.remTags(node.id, tagName)
.then(function() {
return node.id;
});
});
};
/**
* Get a list of tags applied to the specified id
* @param {String} id
* @return {Promise} Resolves to an array of tags
*/
NodeApiService.prototype.getTagsById = function(id) {
return Promise.resolve().then(function() {
assert.isMongoId(id, 'the id must be a valid mongo id');
})
.then(function() {
return waterline.nodes.needByIdentifier(id);
})
.then(function (node) {
return node.tags;
});
};
/**
* Get a list of nodes with the tagName applied to them
* @param {String} tagName
* @return {Promise} Resolves to an array of nodes
*/
NodeApiService.prototype.getNodesByTag = function(tagName) {
return Promise.resolve().then(function() {
assert.string(tagName, 'tag must be a string');
})
.then(function() {
return waterline.nodes.findByTag(tagName);
});
};
/**
* Get a list of all OBMs for the specified Node id
* @param {String} id
* @return {Promise} Resolves to an array of OBMs
*/
NodeApiService.prototype.getObmsByNodeId = function(id) {
return waterline.nodes.getNodeById(id)
.then(function (node){
if (!node) {
throw new Errors.NotFoundError(
'Node not Found ' + id
);
}
return waterline.obms.findAllByNode(id, false);
});
};
/**
* Create an OBM for the specified Node id
* @param {String} id
* @param {Object} obm
* @return {Promise}
*/
NodeApiService.prototype.putObmsByNodeId = function(id, obm) {
return waterline.nodes.getNodeById(id)
.then(function (node) {
if (!node) {
throw new Errors.NotFoundError(
'Node not Found ' + id
);
}
return waterline.obms.upsertByNode(id, obm);
});
};
/**
* Get a list of nodes with the tagName applied to them
* @param {String} tagName
* @return {Promise} Resolves to an array of nodes
*/
NodeApiService.prototype.getNodeByIdentifier = function(identifier) {
return waterline.nodes.findByIdentifier(identifier);
};
return new NodeApiService();
}