domotz-remote-pawn
Version:
Domotz Agent
223 lines (196 loc) • 9.97 kB
JavaScript
/** 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;