domotz-remote-pawn
Version:
Domotz Agent
305 lines (272 loc) • 13.1 kB
JavaScript
/** This file is part of Domotz Agent.
* Copyright (C) 2021 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/>.
*
*
* Created by Fausto Lelli <fausto@domotz.com>
*/
const DISCOVERY_SEMAPHORE = 'SNMP_OVERLAY_SEMAPHORE';
const snmpCommonDiscovery = require('./snmp_common');
var snmpOverlayDiscoveryFactory = function (resourceLocator, snmpGetterFactory, snmpSubtreeFactory) {
'use strict';
var mutex = resourceLocator.utils.mutex;
var lodash = resourceLocator.lodash;
var snmpOverlayDb = resourceLocator.discovery.snmp_overlay_db;
var snmpOverlayDeviceDb = resourceLocator.discovery.snmp_overlay_device_db;
var myConsole = resourceLocator.log.decorateLogs('SNMP_OVERLAY_DISCOVERY');
var interfacesBinding = resourceLocator.interfacesBindingStorage;
var snmpCommon = snmpCommonDiscovery.snmpCommonOptions();
var engineRequest = resourceLocator.engineRequest;
var commandBuilder = require('./command_builder').factory(resourceLocator);
var async = resourceLocator.async;
var snmpCheckStatus = resourceLocator.discovery.snmp_check_status_discovery;
var ipConflict = resourceLocator.networkTools.ipConflictDetection;
var settingsManager = resourceLocator.settingsManager;
var lockDiscovery = function () {
return mutex.lock(DISCOVERY_SEMAPHORE, 1);
};
var unlockDiscovery = function () {
mutex.unlock(DISCOVERY_SEMAPHORE, 1);
};
function getTimestamp() {
return Math.floor(new Date().getTime() / 1000);
}
var deferredSnmpSubtree = function (ip, mac, options, overlayId, oids, cb) {
var snmpCommand = commandBuilder.buildSubtreeCommand(oids, ip);
var commandOptions = snmpCommon.getSnmpOptions(options);
snmpSubtreeFactory(snmpCommand, resourceLocator).execute(function (result) {
if (result.error) {
return cb(null, null);
}
if (myConsole.isEnabledFor('debug')) {
myConsole.debug('Received result for SUBTREE (mac %s) (ip %s) (result %s)', mac, ip, JSON.stringify(result));
}
cb(null, {
device_hw_address: mac,
overlay_id: overlayId,
oid_column: result,
timestamp: getTimestamp(),
});
}, commandOptions);
};
var deferredSnmpBulkGet = function (ip, mac, options, overlayId, oids, cb) {
var snmpCommand = commandBuilder.buildBulkGetCommand(oids, ip);
var commandOptions = snmpCommon.getSnmpOptions(options);
var callbackCalled = false;
snmpGetterFactory(snmpCommand, resourceLocator).execute(function (result) {
if (result.error) {
if (!callbackCalled) {
callbackCalled = true;
cb(null, null);
}
return;
}
if (myConsole.isEnabledFor('debug')) {
myConsole.debug('Received result for GET (mac %s) (ip %s) (result %s)', mac, ip, JSON.stringify(result));
}
if (!callbackCalled) {
callbackCalled = true;
cb(null, {
device_hw_address: mac,
overlay_id: overlayId,
oid_scalar: result,
timestamp: getTimestamp(),
});
}
}, commandOptions);
};
var renderDiscoveryData = function (rawResultList) {
if (myConsole.isEnabledFor('debug')) {
myConsole.debug('Rendering discover raw list : |%s|', JSON.stringify(rawResultList));
}
sendDiscoveryDataToCloud(rawResultList);
return rawResultList;
};
var doDiscovery = function (payload, callback) {
var overlayDeviceDb = [];
if (payload) {
if (myConsole.isEnabledFor('debug')) {
myConsole.debug('PAYLOAD %s', JSON.stringify(payload));
}
overlayDeviceDb = payload.device_overlay;
if (myConsole.isEnabledFor('debug')) {
myConsole.debug('Device Overlay from payload %s', JSON.stringify(overlayDeviceDb));
}
} else {
overlayDeviceDb = snmpOverlayDeviceDb.getOverlayDeviceDB();
}
if (myConsole.isEnabledFor('debug')) {
myConsole.debug('Overlay Device DB: %s', JSON.stringify(overlayDeviceDb));
}
if (overlayDeviceDb === undefined || overlayDeviceDb === null || overlayDeviceDb.length === 0) {
myConsole.info('Overlay device DB empty, null or undefined. Skip discovery');
return;
}
var maxParallel = settingsManager.getSetting('configuration.settings.snmp_overlay_discovery.parallel_limit', 5);
var communitiesProvider = resourceLocator.snmpCommunitiesProvider;
communitiesProvider
.get()
.then(function (communities) {
var functions = [];
var deviceIp;
var freshBindings = interfacesBinding.listFresh(true);
overlayDeviceDb.forEach(function (deviceOverlay) {
var overlayId = deviceOverlay.overlay_id;
var deviceMac = deviceOverlay.device_hw_address;
if (!snmpCheckStatus.isSNMPEnabledForMacAddress(deviceMac)) {
return;
}
if (deviceOverlay.device_ip_address) {
deviceIp = deviceOverlay.device_ip_address;
} else {
var deviceBinding = lodash.find(freshBindings, function (binding) {
return binding.mac === deviceMac;
});
if (!deviceBinding || !deviceBinding.ip) {
myConsole.info('Cannot perform discovery for device: %s, IP binding is missing', deviceMac);
return;
}
deviceIp = deviceBinding.ip;
}
if (ipConflict.checkIfIpInConflict(deviceIp)) {
myConsole.warn('Skip SNMP overlay discovery for ip: %s because there is an IP conflict', deviceIp);
return;
}
myConsole.info('Performing discovery: (mac %s) (ip %s) (overlay_id %s)', deviceMac, deviceIp, overlayId);
var overlay;
if (payload) {
payload.overlays.forEach(function (item) {
if (item.id === overlayId) {
overlay = item;
if (myConsole.isEnabledFor('debug')) {
myConsole.debug('Overlay from payload %s', JSON.stringify(overlay));
}
}
});
} else {
overlay = snmpOverlayDb.getOverlay(overlayId);
}
if (overlay === undefined) {
myConsole.info('Missing overlay info for overlay_id: %s', overlayId);
return;
}
if (overlay.oid_column !== undefined && Object.keys(overlay.oid_column).length > 0) {
myConsole.debug('Check SUBTREE (mac %s) (overlay_id %s) (oids %s)', deviceMac, overlayId, JSON.stringify(overlay.oid_column));
functions.push(deferredSnmpSubtree.bind(null, deviceIp, deviceMac, communities[deviceMac], overlayId, overlay.oid_column));
}
if (overlay.oid_scalar !== undefined && Object.keys(overlay.oid_scalar).length > 0) {
myConsole.debug('Check GET (mac %s) (overlay_id %s) (oids %s)', deviceMac, overlayId, JSON.stringify(overlay.oid_scalar));
functions.push(deferredSnmpBulkGet.bind(null, deviceIp, deviceMac, communities[deviceMac], overlayId, overlay.oid_scalar));
}
});
var startTime = new Date();
async.parallelLimit(functions, maxParallel, function (error, results) {
var elapsed = (new Date() - startTime) / 1000;
myConsole.info('discovery completed in %ss. Parallelism is %s', elapsed, maxParallel);
if (error) {
myConsole.info('Error performing parallelLimit overlay discovery %', error.toString());
if (callback) {
callback();
}
return;
}
var filteredResults = lodash.filter(results, function (v) {
return v !== null;
});
myConsole.info('Uploading result to cloud ...');
if (callback) {
myConsole.info('replying result through callback ...');
callback(filteredResults);
} else {
myConsole.info('Uploading result to cloud ...');
renderDiscoveryData(filteredResults);
}
});
})
.catch(function () {
myConsole.error('Error retrieving communities');
});
};
var discoverOverlay = function (payload, callback) {
if (!lockDiscovery()) {
myConsole.warn('WARNING: SNMP Overlay Discovery already in progress, ignoring the call');
return;
}
try {
doDiscovery(payload, callback);
} catch (e) {
myConsole.error(e.toString());
} finally {
unlockDiscovery();
myConsole.info('Discovery unlocked');
}
};
/**
* Split discoveryData array elements in array of arrays, each element having a byte size less or equal to
* snmp_overlay_discovery.max_chunk_size setting
* @param discoveryData data gathered from devices
* @return an array of chunks
*/
var _splitDiscoveryData = function (discoveryData) {
var MAX_SIZE = settingsManager.getSetting('configuration.settings.snmp_overlay_discovery.max_chunk_size', 524288);
var chunks = [];
var currentChunk = [];
var currentSize = 0;
lodash.forEach(discoveryData, function (item) {
var itemSize = Buffer.byteLength(JSON.stringify(item), 'utf8');
if (currentSize + itemSize > MAX_SIZE) {
chunks.push(currentChunk);
currentChunk = [];
currentSize = 0;
}
currentChunk.push(item);
currentSize += itemSize;
});
if (!lodash.isEmpty(currentChunk)) {
chunks.push(currentChunk);
}
return chunks;
};
/**
* Send to engine all data gathered from devices with multiple HTTP post, each of one having a maximum payload size
* determined by _splitDiscoveryData function
* @param discoveryData data gathered from devices
*/
var sendDiscoveryDataToCloud = function (discoveryData) {
myConsole.info('Overlay discovery FINISHED, Results number to upload %s', lodash.size(discoveryData));
if (myConsole.isEnabledFor('debug')) {
myConsole.debug('Overlay discovery FINISHED, Results to upload %s', JSON.stringify(discoveryData));
}
try {
var discoveryDataChunks = _splitDiscoveryData(discoveryData);
var resource = '/overlay/sample';
lodash.forEach(discoveryDataChunks, function (chunk) {
engineRequest.sendWithPromise(resource, 'POST', { data: chunk }, null, null, { doNotFollow204: true }).catch(function (e) {
myConsole.error('Error sending Overlay data to cloud: %s', JSON.stringify(e));
});
});
} catch (e) {
myConsole.error('Error sending data %s', JSON.stringify(e));
}
};
return {
discoverOverlay: discoverOverlay,
sendDiscoveryDataToEngine: sendDiscoveryDataToCloud,
isDiscoveryInProgress: function () {
return mutex.isLocked(DISCOVERY_SEMAPHORE);
},
};
};
module.exports.factory = snmpOverlayDiscoveryFactory;