UNPKG

domotz-remote-pawn

Version:

Domotz Agent

270 lines (242 loc) 11.7 kB
/** 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@domotz.com on 28/04/2021. * */ const DISCOVERY_SEMAPHORE = 'SNMP_CHECK_STATUS_SEMAPHORE'; const SYS_DESCR_OID = { '1.3.6.1.2.1.1.1.0': 'string' }; const snmpCommonDiscovery = require('./snmp_common'); const mibDiscoveryEventManager = require('../../events/handler/snmpMibDiscovery'); const responseRendering = require('./response_processing').renderSnmpBasicData; var snmpCheckStatusDiscoveryFactory = function (resourceLocator, commandBuilder, snmpGetterFactory) { 'use strict'; var myConsole = resourceLocator.log.decorateLogs(); var lodash = resourceLocator.lodash; var engineRequest = resourceLocator.engineRequest; var communitiesProvider = resourceLocator.snmpCommunitiesProvider; var interfacesBinding = resourceLocator.interfacesBindingStorage; var mutex = resourceLocator.utils.mutex; var async = resourceLocator.async; var host = resourceLocator.discovery.host; var snmpCommon = snmpCommonDiscovery.snmpCommonOptions(); var ipConflict = resourceLocator.networkTools.ipConflictDetection; var parallelLimit = resourceLocator.settingsManager.getSetting('configuration.settings.snmp.discovery.check_status.parallel_limit', 30); var outCacheExpirationHours = resourceLocator.settingsManager.getSetting( 'configuration.settings.snmp.discovery.check_status.sample_expiration_hours', 672 ); //4 weeks var expirationOffset = resourceLocator.settingsManager.getSetting( 'configuration.settings.snmp.discovery.check_status.sample_expiration_random_offset', 30 ); /* WARNING: STATEFUL */ var snmpDiscoverySnapshotData = {}; function snmpBulkGet(snmpCommand, host, options, mac, callback) { var commandOptions = snmpCommon.getSnmpOptions(options); myConsole.debug('Command Options are %s -> %s', mac, JSON.stringify(commandOptions)); var callbackCalled = false; snmpGetterFactory(snmpCommand, resourceLocator).execute(function (result) { myConsole.debug('Received result for %s -> %s', mac, JSON.stringify(result)); result.host = host; result.mac = mac; if (!callbackCalled) { callbackCalled = true; callback(null, result); } }, commandOptions); } var lockDiscovery = function () { return mutex.lock(DISCOVERY_SEMAPHORE, 1); }; var unlockDiscovery = function () { mutex.unlock(DISCOVERY_SEMAPHORE, 1); }; function renderSnmpResults(results) { var snapshotData = {}; for (var i = 0; i < results.length; i++) { var host = results[i].host; var mac = results[i].mac; var data = null; try { data = responseRendering(resourceLocator, results[i]); } catch (e) { data = undefined; } if (host === undefined || mac === undefined || data === null) { myConsole.error('Something went wrong in rendering the SNMP result, skipping'); } else { myConsole.debug('Device result: %s - |%s|', mac, JSON.stringify(data)); if (!data.error) { snapshotData[mac] = { status: 'ON' }; } else if (snapshotData[mac] === undefined) { snapshotData[mac] = { status: 'OFF', error: data.description }; } } } myConsole.debug('Total Results: |%s|', JSON.stringify(snapshotData)); return snapshotData; } function getCacheUntil() { function addRandomOffset(num) { var offset = Math.floor(Math.random() * (expirationOffset + 1)); var sign = Math.random() < 0.5 ? -1 : 1; return num + sign * offset; } var randomizedOutCacheExpirationHours = addRandomOffset(outCacheExpirationHours); myConsole.info('Randomized out cache expiration hours: %s', randomizedOutCacheExpirationHours); return resourceLocator.httpOutCache.expirationInNHours(randomizedOutCacheExpirationHours); } function sendResultToEngine(snmpDiscoveryData) { myConsole.info('Discovery terminated, uploading results ...'); myConsole.info('Discovery terminated, Snapshot (%s) ...', JSON.stringify(snmpDiscoveryData)); for (var deviceMac in snmpDiscoveryData) { if (snmpDiscoveryData.hasOwnProperty(deviceMac) && snmpDiscoveryData[deviceMac].status) { var payload = { status: snmpDiscoveryData[deviceMac].status }; engineRequest.cachedSendWithPromise('/device/' + deviceMac + '/check-status', 'PUT', payload, undefined, undefined, { cache_until: getCacheUntil(), }); } } return snmpDiscoveryData; } function triggerCascadingDiscoveries(snmpDiscoveryData) { var mustTriggerCascadingDiscoveries = false; Object.keys(snmpDiscoveryData).forEach(function (key) { //FIXME? if (snmpDiscoveryData[key].status === 'ON') { mustTriggerCascadingDiscoveries = true; } }); if (mustTriggerCascadingDiscoveries) { myConsole.info('Forcing cascading MIB discovery'); mibDiscoveryEventManager.discoveryForcing(resourceLocator.log); } return snmpDiscoveryData; } var _isSNMPStatusON = function (macAddress) { if (snmpDiscoverySnapshotData[macAddress] && snmpDiscoverySnapshotData[macAddress].status === 'ON') { return true; } myConsole.debug('SNMP Status is not ON for device %s. Discovery skipped. ', macAddress); return false; }; var isSNMPEnabledForMacAddress = function (macAddress) { if (!resourceLocator.settingsManager.isSNMPStatusOnRequiredForSNMPDiscoveries()) { return true; } if (Object.keys(snmpDiscoverySnapshotData).length === 0) { myConsole.debug('snmpDiscoverySnapshotData is empty - try to access %s anyway', macAddress); // If we don't know the status yet we return true optimistically to avoid losing samples. return true; } return _isSNMPStatusON(macAddress); }; var checkStatus = function (address, mac, callback) { communitiesProvider .get() .then(function (communities) { var snmpCommand = commandBuilder.buildBulkGetCommand(SYS_DESCR_OID, address); myConsole.debug('Command for device %s [mac %s] %s ', address, mac, JSON.stringify(snmpCommand)); snmpBulkGet(snmpCommand, address, communities[mac], mac, function (error, results) { results = renderSnmpResults([results]); results = saveDeviceToInternalCache(results); myConsole.debug('Finally, results are %s', JSON.stringify(results)); if (callback) { callback(results); } triggerCascadingDiscoveries(results); }); }) .catch(function () { myConsole.error('Error retrieving communities'); }); }; var saveDeviceToInternalCache = function (results) { Object.keys(results).forEach(function (key) { snmpDiscoverySnapshotData[key] = results[key]; myConsole.debug('Results saved to internal cache %s ', JSON.stringify(snmpDiscoverySnapshotData[key])); }); return results; }; var checkStatusAll = function (callback, errCallback) { if (!lockDiscovery()) { myConsole.warn('WARNING: SNMP Discovery already in progress, ignoring the call'); return; } return communitiesProvider .get() .then(function (communities) { var commands = []; myConsole.debug('Found communities %s ', JSON.stringify(communities)); interfacesBinding.listFresh(true).forEach(function (device) { /* Filter devices by removing the ones present in L3 Discovery Deny list */ if (!host.isDeviceMacInL3DiscoveryDenyList(device.mac)) { var address = device.ip; var snmpCommand = commandBuilder.buildBulkGetCommand(SYS_DESCR_OID, address); myConsole.debug('Command for device %s [mac %s] %s ', device.ip, device.mac, JSON.stringify(snmpCommand)); if (ipConflict.checkIfIpInConflict(address)) { myConsole.warn('Skip SNMP check status discovery for conflicting IP: %s', address); return; } commands.push(snmpBulkGet.bind(null, snmpCommand, address, communities[device.mac], device.mac)); } else { myConsole.debug('Skip SNMP Check Status Discovery for device %s [mac %s] in L3 Discovery Deny List', device.ip, device.mac); } }); var startTime = new Date(); myConsole.info('Starting %s parallel SNMP bulk get tasks', parallelLimit); async.parallelLimit(commands, parallelLimit, function (error, result) { result = renderSnmpResults(result); var discoveryData = saveResultToInternalCache(result); sendResultToEngine(discoveryData); var elapsed = (new Date() - startTime) / 1000; myConsole.info('completed in %s s', elapsed); unlockDiscovery(); if (callback) { callback(snmpDiscoverySnapshotData); } }); }) .catch(function (err, err2) { unlockDiscovery(); myConsole.error('Error on snmp information processing %s %s', err, err2); if (errCallback) { errCallback(err); } }); }; var saveResultToInternalCache = function (result) { snmpDiscoverySnapshotData = lodash.clone(result); return snmpDiscoverySnapshotData; }; var getSnapshotData = function () { return lodash.clone(snmpDiscoverySnapshotData); }; return { checkStatus: checkStatus, checkStatusAll: checkStatusAll, getSnapshotData: getSnapshotData, isDiscoveryInProgress: function () { return mutex.isLocked(DISCOVERY_SEMAPHORE); }, isSNMPEnabledForMacAddress: isSNMPEnabledForMacAddress, saveResultToInternalCache: saveResultToInternalCache, saveDeviceToInternalCache: saveDeviceToInternalCache, }; }; module.exports.factory = snmpCheckStatusDiscoveryFactory;