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