RackHD/on-http

View on GitHub
lib/services/nodes-api-service.js

Summary

Maintainability
F
1 wk
Test Coverage
// 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();
}