domotz-remote-pawn
Version:
Domotz Agent
270 lines (242 loc) • 11.7 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@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;