UNPKG

domotz-remote-pawn

Version:

Domotz Agent

464 lines (411 loc) 20.9 kB
/** This file is part of Domotz Agent. * Copyright (C) 2020 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> */ var t = require('./trie.js'); var path = require('path'); var os = require('os'); const snmpCommonDiscovery = require('./snmp_common'); const DISCOVERY_SEMAPHORE = 'SNMP_MIB_DISCOVERY_SEMAPHORE'; const OID_DB_REMOTE_ENDPOINT = '/oid-catalog'; const OID_ROOT_UPLOAD_ENDPOINT = '/device/[device_mac]/oid-support'; const OID_DB_FILE = path.join(os.tmpdir(), 'oid_db.json'); const SNMP_DISCOVERY_TIMEOUT = 6000; const ONESHOT_DISCOVERY_ITERATION_TIMEOUT = 60000; const ONESHOT_DISCOVERY_MAX_ITERATION = 24; /* Module variables */ var deviceMacToIp = {}; var context = { cycle: 0 }; var snmpDiscoveryFactory = function (resourceLocator, snmpGetterFactory) { 'use strict'; var fileUtils = resourceLocator.utils.file; var crypto = resourceLocator.crypto; var mutex = resourceLocator.utils.mutex; var q = resourceLocator.q; var lodash = resourceLocator.lodash; var interfacesBinding = resourceLocator.interfacesBindingStorage; var snmpCheckStatusDiscovery = resourceLocator.discovery.snmp_check_status_discovery; var engineRequest = resourceLocator.engineRequest; var commandBuilder = require('./command_builder').factory(resourceLocator); var snmpCheckStatus = resourceLocator.discovery.snmp_check_status_discovery; var async = resourceLocator.async; var parallelLimit = resourceLocator.settingsManager.getSetting('configuration.settings.snmp.discovery.mib.parallel_limit', 30); var myConsole = resourceLocator.log.decorateLogs(context); var ipConflict = resourceLocator.networkTools.ipConflictDetection; var snmpCommon = snmpCommonDiscovery.snmpCommonOptions(); /* Instance variables */ var macToSkip = []; var oneshotCounter = 1; var trie = null; var oidDeviceInfoList = {}; function shouldSkipMac(mac) { return lodash.includes(macToSkip, mac); } var lockDiscovery = function () { return mutex.lock(DISCOVERY_SEMAPHORE, 1); }; var unlockDiscovery = function () { mutex.safeUnlock(DISCOVERY_SEMAPHORE, 1); }; var cleanupAndUnlock = function () { macToSkip = []; oneshotCounter = 1; unlockDiscovery(); }; var deferredSnmpBulkGet = function (snmpCommand, host, options, mac, callback) { myConsole.info('Querying host %s: (with mac %s)', host, mac); myConsole.debug(' command is %s ' + JSON.stringify(snmpCommand)); 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 Bytes', host, JSON.stringify(result).length); logResult(host, mac, result); if (!callbackCalled) { callbackCalled = true; callback(null, result); } }, commandOptions); }; function logResult(host, mac, result) { myConsole.debug('Received result for %s', host); result.host = host; result.mac = mac; result.timestamp = new Date().toISOString(); } var getDeviceInfo = function (mac) { var deviceInfo = oidDeviceInfoList[mac]; if (deviceInfo === undefined) { myConsole.debug('Device %s, creating info data', mac); deviceInfo = new t.DeviceOidInfo(); oidDeviceInfoList[mac] = deviceInfo; } return deviceInfo; }; var processDiscoveredData = function (rawResultList) { myConsole.debug('Rendering discover raw list : |%s|', JSON.stringify(rawResultList)); rawResultList.forEach(function (rawResult) { myConsole.debug('Examining result for device ' + rawResult.mac + ': ' + JSON.stringify(rawResult)); var mac = rawResult.mac; var deviceInfo = getDeviceInfo(mac); myConsole.debug('Previous device_info is ' + JSON.stringify(deviceInfo)); for (var i = 0; i < rawResult.length; i++) { var data = rawResult[i]; if (data.valid === true) { myConsole.debug('Searching oid ' + data.oidRoot + ' in trie ....'); var node = trie.find(data.oidRoot); if (node !== null) { myConsole.debug('Found node for oid ' + data.oidRoot + ' node prefix: ' + node.prefix); if (node.isOIDRoot()) { deviceInfo.oidRootValidList.push(data.oidRoot); if (node.isLeaf()) { myConsole.debug('Device %s, oidRoot %s, OID_ROOT (LEAF) found', mac, data.oidRoot); } else { myConsole.debug('Device %s, oidRoot %s, OID_ROOT found', mac, data.oidRoot); deviceInfo.validOidList.push(data.oidRoot); } } else { deviceInfo.validOidList.push(data.oidRoot); myConsole.debug('Device %s, oidRoot %s, valid found (%s)', mac, data.oidRoot, deviceInfo.validOidList.length); } } else { myConsole.debug('Could not found node in trie with oid ' + data.oidRoot); } } } if (deviceInfo.validOidList.length === 0 && macToSkip.indexOf(mac) === -1) { macToSkip.push(mac); } myConsole.debug('Updated device_info is ' + JSON.stringify(deviceInfo)); }); var dataToSend = {}; for (var mac in oidDeviceInfoList) { if (oidDeviceInfoList.hasOwnProperty(mac)) { var deviceInfo = oidDeviceInfoList[mac]; if (shouldSkipMac(mac) && deviceInfo.oidRootValidList.length === 0) { continue; } myConsole.debug('Examining Data to send to engine for device ' + mac); myConsole.debug('Device info ' + JSON.stringify(deviceInfo)); if (deviceInfo.isDataToSend()) { dataToSend[mac] = deviceInfo.oidRootValidList; myConsole.debug('Data to send to engine for device ' + mac + ' is ' + JSON.stringify(dataToSend)); if (Object.keys(dataToSend[mac]).length > 0) { myConsole.debug('SENDING DATA TO ENGINE ' + JSON.stringify(dataToSend)); sendDiscoveryDataToEngine(mac, { oids: dataToSend[mac] }); } } else { myConsole.debug('Data is not to send for device ' + mac); } } } }; var performDiscovery = function () { var communitiesProvider = resourceLocator.snmpCommunitiesProvider; context.cycle++; var deferred = q.defer(); myConsole.info('Performing discovery (oneshot iteration %s) ..... ', oneshotCounter); getOidListToDiscover() .then(function (macToOidList) { myConsole.debug('Received macToOidList ... ' + JSON.stringify(macToOidList)); myConsole.debug('Mapped deviceMacToIp ... ' + JSON.stringify(deviceMacToIp)); if (lodash.isEmpty(macToOidList)) { myConsole.info('Received macToOidList is empty, no need to continue discovery'); deferred.resolve('TERMINATED'); return; } communitiesProvider .get() .then(function (communities) { var devicePromises = []; for (var mac in macToOidList) { if (!snmpCheckStatus.isSNMPEnabledForMacAddress(mac)) { continue; } var ip = deviceMacToIp[mac]; if (ipConflict.checkIfIpInConflict(ip)) { myConsole.warn('Skip SNMP mib discovery for conflicting IP: %s', ip); continue; } myConsole.debug('Examining oid discovery status for device %s (%s)', mac, ip); if (shouldSkipMac(mac)) { myConsole.debug('Analysis already completed or to be avoided for (' + deviceMacToIp[mac] + ') device ' + mac); continue; } var snapshotData = snmpCheckStatusDiscovery.getSnapshotData(); if (snapshotData) { Object.keys(snapshotData).forEach(function (key) { if (key === mac && snapshotData[key] && snapshotData[key].status === 'ON') { myConsole.debug('Device is in readable state in check status discovery ; %s (%s)', key, deviceMacToIp[key]); myConsole.info('Valid SNMP STATUS found, and will be included in Mib Discovery Search %s (%s)', mac, ip); var snmpCommand = commandBuilder.buildNextCommand(macToOidList[mac], ip, SNMP_DISCOVERY_TIMEOUT); myConsole.debug('Command for device %s [mac %s] %s ', ip, mac, JSON.stringify(snmpCommand)); snmpCommand.customTransformer = snmpCommon.transformerTypeInvariant; delete macToOidList[mac]; devicePromises.push(deferredSnmpBulkGet.bind(null, snmpCommand, ip, communities[mac], mac)); } }); } } if (!lodash.isEmpty(devicePromises)) { myConsole.info('About to query all suitable devices ... '); var startTime = new Date(); async.parallelLimit(devicePromises, parallelLimit, function (error, result) { var elapsed = (new Date() - startTime) / 1000; myConsole.info('completed in %s s', elapsed); processDiscoveredData(result); myConsole.info('iteration %d Devices queried, returning ', oneshotCounter); deferred.resolve('CONTINUE'); }); } else { myConsole.info('No suitable device for mib querying from snmp_check_status discovery'); myConsole.info('Terminating globally!'); deferred.resolve('TERMINATED'); } }) .catch(function () { myConsole.error('Error retrieving communities'); }); }) .catch(function (err) { myConsole.error('Skip for now discovery. Do not perform discovery, error %s', err.toString()); deferred.reject('TERMINATED'); }); return deferred.promise; }; var sendLastCheckTimestampOnCloud = function () { engineRequest.sendWithPromise('/mib-discovery', 'PUT'); }; var globalStartTime = 0; var discover = function () { if (!lockDiscovery()) { myConsole.warn('WARNING: SNMP MIB Discovery already in progress, ignoring the call'); return; } myConsole.info('lock successfully acquired'); myConsole.info('Reinitializing deviceInfo data ....'); oidDeviceInfoList = {}; globalStartTime = new Date(); discoveryIteration(oneshotCounter); }; var discoveryIteration = function (oneshotCounter) { return performDiscovery() .then(function (indication) { myConsole.debug('MIB discovery cycle iteration %d concluded, indication is %s', oneshotCounter, indication); if (indication === 'CONTINUE') { myConsole.debug( 'MIB discovery cycle: scheduling next iteration in %d seconds (current is : %d )', ONESHOT_DISCOVERY_ITERATION_TIMEOUT, oneshotCounter ); setTimeout(function () { oneshotCounter++; if (oneshotCounter >= ONESHOT_DISCOVERY_MAX_ITERATION) { myConsole.debug('MIB discovery cycle: max number of iterations reached at : %d ', oneshotCounter); return; } discoveryIteration(oneshotCounter); }, ONESHOT_DISCOVERY_ITERATION_TIMEOUT); } else { var globalElapsed = (new Date() - globalStartTime) / 1000; myConsole.info('all completed in %s s', globalElapsed); myConsole.info('MIB discovery cycle: global termination at iteration %d, indication is %s', oneshotCounter, indication); cleanupAndUnlock(); myConsole.info('snmpCheckDiscovery: lock successfully released'); sendLastCheckTimestampOnCloud(); } }) .catch(function (err, err2) { myConsole.error('MIB discovery cycle Error performing discovery: %s %s', err, err2); cleanupAndUnlock(); myConsole.info(' snmpCheckDiscovery: lock successfully released'); }) .finally(function () { myConsole.info('MIB discovery cycle iteration %d completed ', oneshotCounter); }); }; function getOidDB() { var deferred = q.defer(); function retrieveOidDB() { myConsole.info('Downloading oid root db ...'); engineRequest .sendWithPromise(OID_DB_REMOTE_ENDPOINT, 'GET', {}) .then(function (response) { myConsole.info('Writing oid root db to filesystem ...'); fileUtils.writeFileSafe(OID_DB_FILE, response); deferred.resolve(response); }) .catch(function (err) { myConsole.error('Cant retrieve oid_db ' + err); deferred.reject(); }); } if (fileUtils.getFileStats(OID_DB_FILE)) { myConsole.info('Oid root db file is present (' + OID_DB_FILE + '), checking if it is update ...'); var content = fileUtils.readFile(OID_DB_FILE); if (content) { var localOidRootList = ''; var oidRootList = content.toString().split(','); oidRootList.forEach(function (oid) { localOidRootList += oid; }); var currentMD5 = crypto.createHash('md5').update(localOidRootList).digest('hex'); myConsole.info('Requesting oid root db with MD5 ' + currentMD5); engineRequest .sendWithPromise(OID_DB_REMOTE_ENDPOINT + '?md5=' + currentMD5, 'GET', null, function (response) { if (response.statusCode === 304) { myConsole.info('Oid root db md5 is unchanged, using cached info '); deferred.resolve(oidRootList); } else { retrieveOidDB(); } }) .catch(function (err) { myConsole.error('Cant retrieve oid_db MD5 ' + err); deferred.reject(); }); } else { myConsole.error('Oid root db file is empty'); retrieveOidDB(); } } else { myConsole.info('Oid root db file not present (' + OID_DB_FILE + ')'); retrieveOidDB(); } return deferred.promise; } function initOidTrie() { var deferred = q.defer(); myConsole.info('init Oid Trie'); getOidDB() .then(function (oidRootList) { myConsole.info('Populating OID trie - START, oid number %s', oidRootList.length); trie = new t.TrieNode(); for (var i = 0; i < oidRootList.length; i++) { trie.add(oidRootList[i]); } myConsole.info('Populating OID trie - END'); deferred.resolve(); }) .catch(function () { myConsole.error('Skip for now discovery. Unable to init the trie'); deferred.reject(); }); return deferred.promise; } function getOidListToDiscover() { var deferred = q.defer(); initOidTrie() .then(function () { var macToOidList = {}; interfacesBinding.getLastSnapshot().forEach(function (device) { if (!(device.mac in macToOidList) && !shouldSkipMac(device.mac)) { var deviceInfo = getDeviceInfo(device.mac); myConsole.debug( 'Oid STATUS for Device %s. VALID: ' + deviceInfo.validOidList + ' LEAF: ' + deviceInfo.oidRootValidList + ' Data SENT: ' + deviceInfo.alreadySentData, device.mac ); var toCheck = deviceInfo.getOidToCheck(trie); myConsole.debug('Oids to check for device ' + device.mac + ' : ' + JSON.stringify(toCheck)); if (toCheck.length > 0) { macToOidList[device.mac] = toCheck; deviceMacToIp[device.mac] = device.ip; myConsole.debug('Oid to search for Device %s: ' + macToOidList[device.mac], device.mac); } else if (macToSkip.indexOf(device.mac) === -1) { myConsole.info('Skip searching for Device %s', device.mac); macToSkip.push(device.mac); } } }); deferred.resolve(macToOidList); }) .catch(function (err) { myConsole.error('Skip for now discovery. Unable to get oid list'); myConsole.error(err.message + '\n' + err.stack); deferred.reject(); }); return deferred.promise; } var sendDiscoveryDataToEngine = function (mac, body) { myConsole.debug('uploading oid roots for device %s $s ', mac, JSON.stringify(body)); try { var endpoint = OID_ROOT_UPLOAD_ENDPOINT.replace('[device_mac]', mac); engineRequest.cachedSendWithPromise(endpoint, 'POST', body, null, null, { doNotFollow204: true }).catch(function (e) { myConsole.error('Error sending SNMP data to engine: %s', JSON.stringify(e)); }); } catch (e) { myConsole.error('Error preparing data to send %s', JSON.stringify(e)); } }; return { discover: discover, sendDiscoveryDataToEngine: sendDiscoveryDataToEngine, isDiscoveryInProgress: function () { return mutex.isLocked(DISCOVERY_SEMAPHORE); }, getCurrentSnapshotData: function () { return []; }, }; }; module.exports.factory = snmpDiscoveryFactory;