RackHD/on-dhcp-proxy

View on GitHub
lib/message-handler.js

Summary

Maintainability
D
2 days
Test Coverage
// Copyright 2015, EMC, Inc.

"use strict";

var di = require('di');

module.exports = messageHandlerFactory;
di.annotate(messageHandlerFactory, new di.Provide('DHCP.messageHandler'));
di.annotate(messageHandlerFactory, new di.Inject(
        'DHCP.packet',
        'DHCP.parser',
        'Services.Lookup',
        'Services.Configuration',
        'Protocol.Task',
        'Services.Waterline',
        'Logger',
        'Assert',
        'Errors',
        'Promise',
        '_'
    )
);
function messageHandlerFactory(
    packetUtil,
    parser,
    lookupService,
    configuration,
    taskProtocol,
    waterline,
    Logger,
    assert,
    Errors,
    Promise
) {
    var logger = Logger.initialize(messageHandlerFactory);

    function MessageHandler() {
    }

    MessageHandler.prototype.handleDhcpPacket = function(packet, sendCallback) {
        var self = this;
        var packetData;

        try {
            packetData = parser.parse(packet);
            assert.ok(packetData.chaddr.address);
            assert.ok(packetData.ciaddr);
        } catch (e) {
            logger.error("Error parsing DHCP packet.", { error: e });
            return;
        }

        // Speed up response time by queueing both these async actions
        return Promise.resolve()
        .then(function(){
            /*
             * Workaround for 'Intel Boot Agent' PXE boot issue that
             * no free memory when PXE boot ROM donwload files for severail times.
             * In some physical machine that use Intel Boot Agent PXE boot,
             * when the boot sequence is:
             *      NIC1 PXE boot
             *   -> Donwload iPXE bootfile, and chainload to iPXE boot for NIC1
             *   -> Noting for NIC1 iPXE boot, continue to NIC2 PXE boot
             *   -> Noting for NIC2 PXE boot, continue to NIC1 PXE boot 
             *   -> Donwload iPXE bootfile, and chainload to iPXE boot for NIC1
             *   -> .....(loop as above)
             * NIC ROM firmware will download iPXE bootfile in multiple times,
             * but memory is not freed. So after several times, no free memory left.
             * Console will show 'NBP is too big to fit in free base memory',
             * which will lead to physical machine cannot auto-boot in next time.
             *
             * Workaround this issue by a pre-check. If it's not neccesary,
             * don't need to send iPXE bootfile name to let node download bootfile.
             *
             * The pre-check could be configured so that when there's no this issue,
             * it could be disabled, and also keep the ability of separate DHCP.
             */
            var dhcpSendBootFileName = configuration.get('dhcpSendBootFileName', true);
            if(dhcpSendBootFileName) {
                return self.isBootFileNameSent(packetData);
            } else {
                return true;
            }
        })
        .then(function(isBootFileNameSent) {
            if (isBootFileNameSent) {
                return self.getDefaultBootfile(packetData);
            }
        })
        .then(function(bootFileName) {
            if (bootFileName) {
                var responsePacket = packetUtil.createProxyDhcpAck(packetData, bootFileName);
                sendCallback(responsePacket);
                logger.info(bootFileName + " name is sent to node",
                    { macaddress: packetData.chaddr.address });
            } else {
                logger.info("No bootfile name is sent to node",
                    { macaddress: packetData.chaddr.address });

            }
        })
        .catch(function(err) {
            logger.warning("Failed to get bootfile information for " + packetData.chaddr.address, {
                error: err
            });
        });
    };

    /*
     * Determine if bootfile name should be sent or not
     * in the DHCP packet.
     *
     * @param {Object} packetData - A parsed DHCP packet
     * @returns {Promise}
     */
    MessageHandler.prototype.isBootFileNameSent = function(packetData) {
        var macAddress = packetData.chaddr.address;
        return lookupService.macAddressToNode(macAddress)
        .then(function(node) {
            return node.discovered()
            .then(function(discovered) {
                if (discovered) {
                    return taskProtocol.activeTaskExists(node.id)
                    .then(function() {
                        return taskProtocol.requestProfile(node.id)
                        .then(function () {
                            logger.info("Active task exists, and profile exists",
                                { macaddress: macAddress });
                            return true;
                        })
                        .catch(function () {
                            logger.info("Active task exists, but no profile exists",
                                { macaddress: macAddress });
                            return false;
                        });
                    })
                    .catch(function(){
                        if(node.hasOwnProperty('bootSettings')) {
                            logger.info("There is bootSettings", { macaddress: macAddress });
                            return true;
                        } else {
                            logger.info("Node is discovered, but no active task and boot setttings",
                                { macaddress: macAddress });
                            return false;
                        }
                    });
                } else {
                    logger.info("Node is not discovered", { macaddress: macAddress });
                    return true;
                }
            });
        })
        .catch(function(err){
            if (err instanceof Errors.NotFoundError) {
                logger.info("There is no lookup record for this node", { macaddress: macAddress });
                return true;
            } else {
                logger.error("A lookup failure occur",
                    { macaddress: macAddress,
                      error: err });
                return false;
            }
        });
    };

    /*
     * Determine the appropriate boot file name for a node based on information
     * in the DHCP packet.
     *
     * @member
     * @function
     *
     * @param {Object} packetData - A parsed DHCP packet
     * @returns {Promise}
     */
    MessageHandler.prototype.getDefaultBootfile = function(packetData) {
        assert.object(packetData);
        assert.object(packetData.options);
        assert.string(packetData.chaddr.address);

        logger.debug("DHCP packetData:", {
            userClass: packetData.options.userClass,
            vendorClassIdentifier: packetData.options.vendorClassIdentifier,
            archType: packetData.options.archType
        });

        // Defaults if we don't have an override from an active task
        if (packetData.options.userClass === "MonoRail") {
            return "http://" + configuration.get('apiServerAddress', '10.1.1.1') + ":" +
                           configuration.get('apiServerPort', '80') + "/api/current/profiles";
        }
        // expect to start w/ 'Arista'
        if (packetData.options.vendorClassIdentifier &&
                    packetData.options.vendorClassIdentifier.indexOf('Arista') === 0) {
            // Arista skips the TFTP download step, so just hit the
            // profiles API directly to get a profile from an active task
            // if there is one.
            return "http://" + configuration.get('apiServerAddress', '10.1.1.1') + ":" +
                           configuration.get('apiServerPort', '80') + "/api/current/profiles" +
                           "?macs=" + packetData.chaddr.address.toLowerCase();
        }
        // If the mac belongs to a mellanox card, assume that it already has
        // Flexboot and don't hand down an iPXE rom 
        const mellanox = ['f4:52:14', 'ec:0d:9a', 'e4:1d:2d', '7c:fe,90', '24:8a:07', '00:25:8b', '00:02:c9'];
        if (packetData.chaddr.address &&
                mellanox.some(function(v){return packetData.chaddr.address.toLowerCase().indexOf(v) === 0; })) {
            return "http://" + configuration.get('apiServerAddress', '10.1.1.1') + ":" +
                           configuration.get('apiServerPort', '80') + "/api/current/profiles";
        }
        // Same bug as above but for the NICs
        if (packetData.chaddr.address &&
                packetData.chaddr.address.toLowerCase().indexOf('ec:a8:6b') === 0) {
            logger.info("Sending down monorail.intel.ipxe for mac address associated with NUCs.");
            return 'monorail.intel.ipxe';
        }

        /*
         * System architecture type list from RFC4578:
         *             Type   Architecture Name
         *             ----   -----------------
         *              0    Intel x86PC
         *              1    NEC/PC98
         *              2    EFI Itanium
         *              3    DEC Alpha
         *              4    Arc x86
         *              5    Intel Lean Client
         *              6    EFI IA32
         *              7    EFI BC
         *              8    EFI Xscale
         *              9    EFI x86-64
         */
        if (packetData.options.vendorClassIdentifier) {
            if (packetData.options.archType === 7 || packetData.options.archType === 9) {
                return 'monorail-efi64-snponly.efi';
            }
            if (packetData.options.archType === 6) {
                return 'monorail-efi32-snponly.efi';
            }

            /* Notes for UNDI
             * 1) Some NICs support UNDI driver, it needs chainload ipxe's undionly.kpxe file to
             * bootup otherwise it will hang using monorail.ipxe. NOTE: if 'UNDI' is in class
             * identifier but cannot boot for some NICs, please use MAC address or other
             * condition to judge if use monorail-undionly.kpxe or not, or report it as a bug for
             * this NIC.
             *
             * 2) Some Notes from PXE spec about DHCP options:
             * PXEClient:Arch:xxxxx:UNDI:yyyzzz used for  transactions between client and server.
             * (These strings are case sensitive. This field must not be null terminated.)
             * The information from tags 93 and 94 is embedded in the Class Identifier string
             * xxxxx = Client Sys Architecture 0 . 65535
             * yyy = UNDI Major version 0 . 255
             * zzz = UNDI Minor version 0 . 255
             *
             * The Client Network Interface Identifier specifies the version of the UNDI API
             * (described below) that will support a universal network driver. The UNDI interface
             * must be supported and its version reported in tag #94.
             *
             */
            if (packetData.options.archType === 0 &&
                    packetData.options.vendorClassIdentifier.indexOf('PXEClient:Arch:00000:UNDI') === 0) { // jshint ignore:line
                return 'monorail-undionly.kpxe';
            }

            return 'monorail.ipxe';
        }
    };

    return new MessageHandler();
}