RackHD/on-taskgraph

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

Summary

Maintainability
F
4 days
Test Coverage
// Copyright © 2017 Dell Inc. or its subsidiaries. All Rights Reserved.

'use strict';

var di = require('di'),
    ejs = require('ejs');


module.exports = profileApiServiceFactory;
di.annotate(profileApiServiceFactory, new di.Provide('Http.Services.Api.Profiles'));
di.annotate(profileApiServiceFactory,
    new di.Inject(
        'Promise',
        'Http.Services.Api.Workflows',
        'Protocol.Task',
        'Protocol.Events',
        'Services.Waterline',
        'Services.Configuration',
        'Services.Lookup',
        'Logger',
        'Errors',
        '_',
        'Profiles',
        'Services.Environment',
        'Http.Services.Swagger',
        'Constants',
        'Assert'
    )
);
function profileApiServiceFactory(
    Promise,
    workflowApiService,
    taskProtocol,
    eventsProtocol,
    waterline,
    configFile,
    lookupService,
    Logger,
    Errors,
    _,
    profiles,
    Env,
    swaggerService,
    Constants,
    assert
) {

    var logger = Logger.initialize(profileApiServiceFactory);

    function ProfileApiService() {
    }

    // Helper to convert property kargs into an ipxe friendly string.
    ProfileApiService.prototype.convertProperties = function(properties) {
        properties = properties || {};

        if (properties.hasOwnProperty('kargs')) {
            // This a promotion of the kargs property
            // for DOS disks (or linux) for saving
            // the trouble of having to write a
            // bunch of code in the EJS template.
            if(typeof properties.kargs === 'object') {
                properties.kargs = _.map(
                    properties.kargs, function (value, key) {
                        return key + '=' + value;
                    }).join(' ');
            }
        } else {
            // Ensure kargs is set for rendering.
            properties.kargs = null;
        }

        return properties;
    };

    ProfileApiService.prototype.getMacs = function(macs) {
        return _.flattenDeep([macs]);
    };

    /**
     * Get macAddress in HTTP request
     * @param {Object} query      the query in HTTP request
     * @param {String} requestIp  the IP of the HTTP request
     * @return {Promise} Resolves to macAddress if found, otherwise undefined.
     */
    ProfileApiService.prototype.getMacAddressInRequest = function(query, requestIp) {
        assert.object(query);
        assert.string(requestIp);

        if (query.macs && query.ips) {
            var macAddresses = _.flattenDeep([query.macs]);
            var ipAddresses = _.flattenDeep([query.ips]);

            var index = _.findIndex(ipAddresses, function(ip) {
                return (ip && (ip === requestIp));
            });

            if(index >= 0 && macAddresses[index]) {
                return Promise.resolve(macAddresses[index]);
            }
        }

        return Promise.resolve();
    };

    ProfileApiService.prototype.setLookup = function(ipAddress, macAddress, proxyIp, proxyPort) {
        return lookupService.setIpAddress(ipAddress, macAddress)
        .then(function() {
            if (proxyIp) {
                var proxy = 'http://%s:%s'.format(proxyIp, proxyPort);
                return waterline.lookups.upsertProxyToMacAddress(proxy, macAddress);
            }
        });
    };

    ProfileApiService.prototype.getNode = function(macAddresses, options) {
        var self = this;
        return waterline.nodes.findByIdentifier(macAddresses)
            .then(function (node) {
                if (node) {
                    return node.discovered()
                        .then(function(discovered) {
                            if (!discovered) {
                                return taskProtocol.activeTaskExists(node.id)
                                    .then(function() {
                                        return node;
                                    })
                                    .catch(function() {
                                        return self.runDiscovery(node, options);
                                    });
                            } else {
                                // We only count a node as having been discovered if
                                // a node document exists AND it has any catalogs
                                // associated with it
                                return node;
                            }

                        });
                } else {
                    return self.createNodeAndRunDiscovery(macAddresses, options);
                }
            });
    };

    ProfileApiService.prototype.runDiscovery = function(node, options) {
        var self = this;
        var configuration;


        if (node.type === 'switch') {
            configuration = self.getSwitchDiscoveryConfiguration(node, options.switchVendor);
        } else {
            var rebootCode = 1; //ipmi power cycle
            var setObm = configFile.get('autoCreateObm', 'false');
            var skipReboot = configFile.get('skipResetPostDiscovery', 'false');
            if (skipReboot === 'true') {
                rebootCode = 127; // skip reset but terminate bootstrap
            }
            if (setObm === 'true') {
                skipReboot = 'true';
            } else {
                skipReboot = 'false';
            }
            var skipPollers = configFile.get('skipPollersCreation', 'false');
            configuration = {
                name: configFile.get('discoveryGraph', 'Graph.SKU.Discovery'),
                options: {
                    defaults: {
                        graphOptions: {
                            target: node.id,
                            'skip-reboot-post-discovery' : {
                                skipReboot: skipReboot
                            },
                            'shell-reboot': {
                                rebootCode: rebootCode
                            }
                        },
                        nodeId: node.id
                    },
                    'skip-pollers': {
                        skipPollersCreation: skipPollers
                    },
                    'obm-option' : {
                        autoCreateObm: setObm
                    }
                }
            };
        }

        // If there is an api proxy add it to the context
        lookupService.nodeIdToProxy(node.id).then( function(proxy) {
            if(proxy) {
                configuration.context = {proxy: proxy};
            }
        });

        // The nested workflow holds the lock against the nodeId in this case,
        // so don't add it as a target to the outer workflow context
        return workflowApiService.createAndRunGraph(configuration, null)
            .then(function() {
                return self.waitForDiscoveryStart(node.id);
            })
            .then(function() {
                return node;
            });
    };

    ProfileApiService.prototype.getSwitchDiscoveryConfiguration = function(node, vendor) {
        var configuration = {
            name: 'Graph.SKU.Switch.Discovery.Active',
            options: {
                defaults: {
                    graphOptions: {
                        target: node.id
                    },
                    nodeId: node.id
                },
                'vendor-discovery-graph': {
                    graphName: null
                }
            }
        };

        vendor = vendor.toLowerCase();

        if (vendor === 'cisco') {
            configuration.options['vendor-discovery-graph'].graphName =
                'Graph.Switch.Discovery.Cisco.Poap';
        } else if (vendor === 'brocade') {
            configuration.options['vendor-discovery-graph'].graphName =
                'Graph.Switch.Discovery.Brocade.Ztp';
        } else if (vendor === 'arista') {
            configuration.options['vendor-discovery-graph'].graphName =
                'Graph.Switch.Discovery.Arista.Ztp';
        } else if (vendor === 'onie') {
            configuration.options['vendor-discovery-graph'].graphName =
                'Graph.Switch.Discovery.Dell.Onie';          
        } else if (vendor === 'dell') {
            configuration.options['vendor-discovery-graph'].graphName =
                'Graph.Switch.Discovery.Dell.Bmp';          
        } else {
            throw new Errors.BadRequestError('Unknown switch vendor ' + vendor);
        }

        return configuration;
    };

    ProfileApiService.prototype.createNodeAndRunDiscovery = function(macAddresses, options) {
        var self = this;
        var node;
        return Promise.resolve().then(function() {
            return waterline.nodes.create({
                name: macAddresses.join(','),
                identifiers: macAddresses,
                type: options.type
            });
        }).tap(function(_node) {
            return eventsProtocol.publishNodeEvent(_node, 'added');
        }).then(function (_node) {
                node = _node;

                return Promise.resolve(macAddresses).each(function (macAddress) {
                    return waterline.lookups.upsertNodeToMacAddress(node.id, macAddress);
                });
            })
            .then(function () {
                // Setting newRecord to true allows us to
                // render the redirect again to avoid refresh
                // of the node document and race conditions with
                // the state machine changing states.
                node.newRecord = true;

                return self.runDiscovery(node, options);
            });
    };

    // Quick and dirty extra two retries for the discovery graph, as the
    // runTaskGraph promise gets resolved before the tasks themselves are
    // necessarily started up and subscribed to bus events.
    ProfileApiService.prototype.waitForDiscoveryStart = function(nodeId) {
        var retryRequestProperties = function(error) {
            if (error instanceof Errors.RequestTimedOutError) {
                return taskProtocol.requestProperties(nodeId);
            } else {
                throw error;
            }
        };

        return taskProtocol.requestProperties(nodeId)
            .catch(retryRequestProperties)
            .catch(retryRequestProperties);
    };

    ProfileApiService.prototype._handleProfileRenderError = function(errMsg, type, status) {
        var err = new Error("Error: " + errMsg);
        err.status = status || 500;
        throw err;
    };

    ProfileApiService.prototype.getProfileFromTaskOrNode = function(node, vendor) {
        var self = this;
        var defaultProfile;

        if (node.type === 'switch') {
            // Unlike for compute nodes, we don't need to or have the capability
            // of booting into a microkernel, so just send down the
            // python script right away, and start downloading
            // and executing tasks governed by the switch-specific
            // discovery workflow.
            if(vendor === 'onie'){
                defaultProfile = 'dell-onie.sh';
            } else if(vendor === 'dell'){
                defaultProfile = 'dell-bmp.sh';
            } else {
                defaultProfile = 'taskrunner.py';
            }
        } else {
            defaultProfile = 'redirect.ipxe';
        }

        return workflowApiService.findActiveGraphForTarget(node.id)
            .then(function (taskgraphInstance) {
                if (taskgraphInstance) {
                    return taskProtocol.requestProfile(node.id)
                        .catch(function(err) {
                            if (node.type === 'switch') {
                                return null;
                            } else {
                                throw err;
                            }
                        })
                        .then(function(profile) {
                            return [profile, taskProtocol.requestProperties(node.id)];
                        })
                        .spread(function (profile, properties) {
                            var _options;
                            if (node.type === 'compute' || node.type === 'redfish') {
                                _options = self.convertProperties(properties);
                            } else if (node.type === 'switch') {
                                var switchVendor;
                                if(taskgraphInstance.injectableName === "Graph.Switch.Discovery.Arista.Ztp"){
                                    switchVendor = "arista";
                                }else if(taskgraphInstance.injectableName === "Graph.Switch.Discovery.Brocade.Ztp"){
                                    switchVendor = "brocade";
                                }else if(taskgraphInstance.injectableName === "Graph.Switch.Discovery.Cisco.Poap"){
                                    switchVendor = "cisco";
                                } else if(taskgraphInstance.injectableName === "Graph.Switch.Discovery.Dell.Onie"){
                                    switchVendor = "onie";
                                } else if(taskgraphInstance.injectableName === "Graph.Switch.Discovery.Dell.Bmp"){
                                    switchVendor = "dell";
                                }
                                _options = {
                                    identifier: node.id,
                                    switchVendor : switchVendor
                                };
                            }
                            return {
                                profile: profile || defaultProfile,
                                options: _options,
                                context: taskgraphInstance.context
                            };
                        })
                        .catch(function (e) {
                            logger.warning("Unable to retrieve workflow properties or profiles", {
                                error: e,
                                id: node.id,
                                taskgraphInstanceId: taskgraphInstance.instanceId
                            });
                            return self._handleProfileRenderError(
                                'Unable to retrieve workflow properties or profiles', node.type, 503);
                        });
                } else {
                    if (_.has(node, 'bootSettings')) {
                        if (_.has(node.bootSettings, 'options') &&
                            _.has(node.bootSettings, 'profile')) {
                            return {
                                profile: node.bootSettings.profile || 'redirect.ipxe',
                                options: node.bootSettings.options
                            };
                        } else {
                            return self._handleProfileRenderError(
                                'Unable to retrieve valid node bootSettings', node.type);
                        }
                    } else {
                        return {
                            profile: 'ipxe-info.ipxe',
                            options: { message:
                                'No active workflow and bootSettings, continue to boot' },
                            context: undefined
                        };
                    }
                }
            });
    };

    ProfileApiService.prototype.renderProfile = function (profile, req, res) {
        var scope = res.locals.scope;
        var options = profile.options || {};
        var graphContext = profile.context || {};

        var promises = [
            swaggerService.makeRenderableOptions(req, res, graphContext,
                profile.ignoreLookup),
            profiles.get(profile.profile, true, scope)
        ];

        if (profile.profile.endsWith('.ipxe')) {
            promises.push(profiles.get('boilerplate.ipxe', true, scope));
        }

        return Promise.all(promises).spread(
            function (localOptions, contents, boilerPlate) {
                options = _.merge({}, options, localOptions);
                // Render the requested profile + options. Don't stringify undefined.
                return ejs.render((boilerPlate || '') + contents, options);
            }
        );
    };

    ProfileApiService.prototype.getProfiles = function(req, query, res) {
        var self = this;
        var ipAddress = res.locals.ipAddress;
        return self.getMacAddressInRequest(query, ipAddress)
        .then(function(macAddress) {
            if(macAddress) {
                res.locals.macAddress = macAddress;
                var proxyIp = req.get(Constants.HttpHeaders.ApiProxyIp);
                var proxyPort = req.get(Constants.HttpHeaders.ApiProxyPort);
                return self.setLookup(ipAddress, macAddress, proxyIp, proxyPort);
            }
        })
        .then(function() {
            var macs = req.query.mac || req.query.macs;
            if (macs) {
                var macAddresses = self.getMacs(macs);
                var options = {
                    type: 'compute'
                };

                return self.getNode(macAddresses, options)
                    .then(function (node) {
                        return self.getProfileFromTaskOrNode(node)
                            .then(function (render) {
                                return _.defaults(render, {
                                    ignoreLookup: res.locals.macAddress ? true : false
                                });
                            });
                    });
            } else {
                return { profile: 'redirect.ipxe', ignoreLookup: true };
            }
        })
        .catch(function (err) {
            if (!err.status) {
                throw new Errors.InternalServerError(err.message);
            } else {
                throw err;
            }
        });
    };

    ProfileApiService.prototype.getProfilesSwitchVendor = function(
        requestIp, vendor
    ) {
        var self = this;
        return waterline.lookups.findOneByTerm(requestIp)
            .then(function(record) {
                return record.macAddress;
            })
            .then(function(macAddress) {
                return self.getMacs(macAddress);
            })
            .then(function(macAddresses) {
                var options = {
                    type: 'switch',
                    switchVendor: vendor
                };
                return self.getNode(macAddresses, options);
            })
            .then(function(node) {
                return self.getProfileFromTaskOrNode(node, vendor);
            })
            .catch(function (err) {
                throw err;
            });
    };

    ProfileApiService.prototype.postProfilesSwitchError = function(error) {
        logger.error('SWITCH ERROR DEBUG ', error);
    };

    return new ProfileApiService();
}