UNPKG

domotz-remote-pawn

Version:

Domotz Agent

223 lines (196 loc) 9.97 kB
/** This file is part of Domotz Agent. * Copyright (C) 2016 Domotz Ltd * * Domotz Agent is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Domotz Agent is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Domotz Agent. If not, see <http://www.gnu.org/licenses/>. * * * This module has the responsibility to send the discovered IP devices to the engine, choosing between a full or * a delta report following an internal logic * * Created by Iacopo Papalini <iacopo@domotz.com> on 19/05/16. */ const engineIpDiscoveryReportStrategyFactory = function (resourceLocator) { const MAC_ADDRESS_REGEX = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; const interfacesBindingStorage = resourceLocator.interfacesBindingStorage; const host = resourceLocator.discovery.host; const discoveryCounter = resourceLocator.discovery.counter; var iteration = 0; const lodash = resourceLocator.lodash; const baseConsole = resourceLocator.log.decorateLogs(); var myConsole = baseConsole; const dhcpRequestsCounters = resourceLocator.DHCPRequestCounters; var dhcpDiscoveredDevicesToBeReported = {}; var devicesAlreadyDiscovered = {}; function setIteration(i) { iteration = i; myConsole = baseConsole.decorate(String(iteration)); } function isValidMacAddress(mac) { return MAC_ADDRESS_REGEX.test(mac); } function addAccumulatedDiscoveryData(keys, payload) { var discoveryData = resourceLocator.discovery.accumulator.fetch(keys); Object.keys(discoveryData).forEach(function (mac) { if (payload[mac] && discoveryData[mac] && payload[mac].length) { myConsole.info('Adding protocols %s to device %s', Object.keys(discoveryData[mac]), mac); payload[mac][0].data = discoveryData[mac]; } }); return payload; } function addDhcpDiscoveredDevices(devicesFromNmap) { var dhcpDeviceDiscoveryWaitCycles = resourceLocator.settingsManager.getSetting('configuration.settings.dhcp.device_discovery_wait_cycles', 5); var macsFromNmap = Object.keys(devicesFromNmap); const macsFromDhcp = dhcpRequestsCounters.retrieveMacAddresses(); var devicesToReport = lodash.cloneDeep(devicesFromNmap); // update the set of devices known through nmap discovery. If a device has been discovered // through nmap we must delete it from the set of DHCP-only devices to report macsFromNmap.forEach(function (mac) { devicesAlreadyDiscovered[mac] = true; if (mac in dhcpDiscoveredDevicesToBeReported) { delete dhcpDiscoveredDevicesToBeReported[mac]; } }); // find out which DHCP-only MAC addresses to report macsFromDhcp.forEach(function (mac) { if (!(mac in devicesAlreadyDiscovered) && !(mac in dhcpDiscoveredDevicesToBeReported) && isValidMacAddress(mac)) { dhcpDiscoveredDevicesToBeReported[mac] = dhcpDeviceDiscoveryWaitCycles; } }); // decrement the counter for DHCP-only devices, if the counter reaches 0 report it to the cloud lodash.forOwn(dhcpDiscoveredDevicesToBeReported, function (value, mac) { if (dhcpDiscoveredDevicesToBeReported[mac] === 0) { myConsole.info('adding binding with no IP address for %s', mac); devicesToReport[mac] = [{ ip: null, host_name: null }]; devicesAlreadyDiscovered[mac] = true; delete dhcpDiscoveredDevicesToBeReported[mac]; } else { dhcpDiscoveredDevicesToBeReported[mac]--; myConsole.info('Decrementing counter for %s: %s', mac, dhcpDiscoveredDevicesToBeReported[mac]); } }); return devicesToReport; } function removeUnexpectedExternalDevices(payload) { /* We need to make sure we are not reporting status information on devices belonging to external * hosts/subnets that are not currently configured (e.g., this may happen when a new configuration is * retrieved from the server and a discovery is already in progress based on the old configuration). */ if (resourceLocator.settingsManager.isLayer3ScanEnabled()) { return payload; } return lodash.omitBy(payload, function (dev, key) { if (!dev) { return false; } var omit = (key.indexOf(interfacesBindingStorage.CUSTOM_NETWORK_PREFIX) === 0 && !host.deviceBelongsToExternalSubnet(dev)) || (key.indexOf(interfacesBindingStorage.EXTERNAL_HOST_PREFIX) === 0 && !host.isMacInExternalHostsInterfaces(key)); if (omit) { myConsole.warn('External subnet, Skipping report for custom mac: ' + key); } return omit; }); } return (function () { var ret = { setIteration: setIteration, reportDiscovery: function () { var seq = discoveryCounter.getNextSequenceNumber(); var payload = discoveryCounter.isFullDiscovery() ? ret.createFullPayload() : ret.createDeltaPayload(); var dhcpDeviceDiscoveryEnabled = resourceLocator.settingsManager.getSetting('configuration.settings.dhcp_device_discovery', false); if (dhcpDeviceDiscoveryEnabled) { payload = addDhcpDiscoveredDevices(payload); } payload = removeUnexpectedExternalDevices(payload); var deferred = resourceLocator.q.defer(); if (isToSend(payload, seq)) { var keys = Object.keys(payload); payload = addAccumulatedDiscoveryData(keys, payload); myConsole.debug('Sending IP Network device discovery report to engine. Sequence number: ' + seq); myConsole.debug('Nmap Payload is: '+ JSON.stringify(payload)); resourceLocator.engineRequest.send( '/device/ip?seq=' + seq, 'PUT', payload, function () { myConsole.info('Report successfully sent to engine. Sequence number: ' + seq); discoveryCounter.incrementCounter(); resourceLocator.discovery.accumulator.flush(keys); return deferred.resolve(); }, function (err) { myConsole.warn( 'Error reporting IP network devices ' + 'to engine. Sequence number: ' + seq + ' - Rolling back sequence number' ); return deferred.reject(err); } ); } else { myConsole.info('No changes in IP network - no report will be sent to the agent'); discoveryCounter.incrementCounter(); deferred.resolve(); } function isToSend(payload, seq) { return seq === 0 || isNotEmpty(payload); } function isNotEmpty(payload) { // http://stackoverflow.com/questions/679915/how-do-i-test-for-an-empty-javascript-object return !(Object.keys(payload).length === 0 && payload.constructor === Object); } return deferred.promise; }, createFullPayload: function () { return bindingListToDictionary(interfacesBindingStorage.listFresh()); }, createDeltaPayload: function () { var payload = {}; var fresh = bindingListToDictionary(interfacesBindingStorage.listFresh()); var addStaleInfo = function (stale) { var mac = stale.mac; if (fresh[mac]) { if (payload[mac] === undefined) { payload[mac] = []; } payload[mac] = payload[mac].concat(fresh[mac]); } if (payload[mac] !== undefined) { return; } payload[mac] = null; }; var addNewInfo = function (newOne) { var mac = newOne.mac; // new device is always fresh - add it with all the fresh IP addresses payload[mac] = fresh[mac]; }; interfacesBindingStorage.listStale().forEach(addStaleInfo); interfacesBindingStorage.listNew().forEach(addNewInfo); return payload; }, dhcpDiscoveredDevicesToBeReported: dhcpDiscoveredDevicesToBeReported, }; function bindingListToDictionary(list) { var ret = {}; list.forEach(function (binding) { if (ret[binding.mac] === undefined) { ret[binding.mac] = []; } ret[binding.mac].push({ ip: binding.ip, host_name: binding.hostname }); }); return ret; } return ret; })(); }; module.exports.engineIpDiscoveryReportStrategyFactory = engineIpDiscoveryReportStrategyFactory;