domotz-remote-pawn
Version:
Domotz Agent
426 lines (362 loc) • 15.5 kB
JavaScript
/** This file is part of Domotz Agent.
* Copyright (C) 2016 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 Andrea Azzarà <a.azzara@domotz.com> on 03/12/18.
*/
;
function rtdScan(resourceLocator) {
var os = resourceLocator.os;
var path = resourceLocator.path;
var fileUtils = resourceLocator.utils.file;
var async = resourceLocator.async;
var ping = resourceLocator.netPing;
var host = resourceLocator.discovery.host;
var interfacesBindingStorage = resourceLocator.interfacesBindingStorage;
var rtdResultProcess = resourceLocator.networkTools.rtdProcessor;
var fsPath = path.join(os.tmpdir(), 'domotz_rtd_samples.json');
var changed = false;
var executionCounter = 3;
var maxBurstsInParallel = 30;
var sessionId = 0;
var MAX_WINDOWS_SIZE = 1000;
var PING_PACKET_SIZE = 16;
var samplesExpiration = 15 * 60; // 15 minutes
var globalSamples = {};
var minLivePingCount = resourceLocator.lodash.get(resourceLocator, 'configuration.settings.rtd.live_ping.count.min', 1);
var maxLivePingCount = resourceLocator.lodash.get(resourceLocator, 'configuration.settings.rtd.live_ping.count.max', 100);
var minLivePingIntervalMs = resourceLocator.lodash.get(resourceLocator, 'configuration.settings.rtd.live_ping.interval_ms.min', 100);
var maxLivePingIntervalMs = resourceLocator.lodash.get(resourceLocator, 'configuration.settings.rtd.live_ping.interval_ms.max', 5000);
var minLivePingTimeoutMs = resourceLocator.lodash.get(resourceLocator, 'configuration.settings.rtd.live_ping.timeout_ms.min', 100);
var maxLivePingTimeoutMs = resourceLocator.lodash.get(resourceLocator, 'configuration.settings.rtd.live_ping.timeout_ms.max', 5000);
var myConsole = resourceLocator.log.decorateLogs();
function pingDevice(ipAddress, delay, session, cb) {
setTimeout(function () {
myConsole.verbose('pingDevice starting session for %s with delay %s, sessionId %s: ', ipAddress, delay, sessionId);
var cbCalled = false;
session.on('error', function (error) {
myConsole.error('pingDevice error', JSON.stringify(error));
if (!cbCalled) {
cbCalled = true;
cb(null, -1);
}
});
session.pingHost(ipAddress, function (error, target, sent, rcvd) {
if (error) {
myConsole.debug('%s ping timeout', ipAddress);
if (!cbCalled) {
cbCalled = true;
cb(null, -1);
}
return;
}
var resultMs = rcvd - sent;
myConsole.verbose('%s ping completed in: %s ', ipAddress, resultMs);
if (!cbCalled) {
cbCalled = true;
cb(null, resultMs);
}
});
}, delay);
}
function pingDeviceBurst(binding, session, params, cb) {
myConsole.verbose('Scheduling pingDeviceBurst for %s: ', binding.ip);
var burstPingList = [];
var delay = 0;
for (var i = 0; i < params.burst_size; i++) {
burstPingList.push(pingDevice.bind(null, binding.ip, delay, session));
delay += params.t_ping_interval_ms;
}
async.parallel(burstPingList, function (error, results) {
if (error) {
myConsole.warn('pingDeviceBurst error: %s', JSON.stringify(error));
cb(error);
return;
}
var out = {};
out[binding.mac] = results;
myConsole.verbose('pingDeviceBurst completed for %s', binding.ip);
cb(null, out);
});
}
function scanHosts() {
if (!ping) {
myConsole.info('Native Ping module not available, skip ping scan execution');
return;
}
var params = resourceLocator.settingsManager.getRTDConfiguration();
if (!params) {
return;
}
executionCounter++;
if (executionCounter % params.t_ping_fast_discoveries !== 0) {
myConsole.info('Skipping ping scan execution counter=%s', executionCounter);
return;
}
var getPingOptions = function () {
var session = sessionId;
sessionId++;
return {
networkProtocol: ping.NetworkProtocol.IPv4,
packetSize: PING_PACKET_SIZE,
retries: 0,
sessionId: session,
timeout: params.t_ping_timeout_ms,
};
};
var freshBindings = interfacesBindingStorage.listFresh();
var pingFuncList = [];
var uniqueBindings = [];
var session = ping.createSession(getPingOptions());
var sessionId = session.sessionId;
freshBindings.forEach(function (binding) {
if (uniqueBindings.indexOf(binding.mac) === -1) {
if (!host.isDeviceMacInL3DiscoveryDenyList(binding.mac)) {
//push a single ip for each mac, only if the device is not in L3 Discovery Deny list
pingFuncList.push(pingDeviceBurst.bind(null, binding, session, params));
uniqueBindings.push(binding.mac);
} else {
myConsole.debug('RTD Scan skipped for device IP = %s [ MAC = %s ] because in L3 Discovery Deny List', binding.ip, binding.mac);
}
}
});
session.on('close', function () {
myConsole.verbose('socket closed: %s', sessionId);
});
if (uniqueBindings.length === 0) {
session.close();
return;
}
async.parallelLimit(pingFuncList, maxBurstsInParallel, function (err, results) {
session.close();
if (err) {
myConsole.warn('Parallel ping error' + err.toString());
return;
}
myConsole.verbose('Parallel ping res %s', JSON.stringify(results));
results.forEach(function (sample) {
for (var mac in sample) {
if (sample.hasOwnProperty(mac)) {
var validSamples = sample[mac].filter(function (value) {
return value >= 0;
});
var packetLost = sample[mac].filter(function (value) {
return value < 0;
}).length;
var count = sample[mac].length;
if (!(mac in globalSamples)) {
globalSamples[mac] = {
rtd: validSamples,
packetLost: packetLost,
count: count,
};
} else {
var currentSamples = globalSamples[mac].rtd;
globalSamples[mac].rtd = globalSamples[mac].rtd.concat(validSamples);
globalSamples[mac].packetLost += packetLost;
globalSamples[mac].count += count;
if (currentSamples.length > MAX_WINDOWS_SIZE) {
var diff = currentSamples.length - params.window_size_samples;
globalSamples[mac].rtd.splice(0, diff);
}
}
}
}
});
changed = true;
myConsole.verbose('samples %s', JSON.stringify(globalSamples, null));
rtdResultProcess.processResults(globalSamples, executionCounter, params);
});
}
function getGlobalSamples() {
return globalSamples;
}
function storeToFS() {
if (Object.keys(globalSamples).length > 0 && changed) {
myConsole.info('Storing RTD samples to fs');
fileUtils.writeFileSafe(
fsPath,
JSON.stringify({
d: globalSamples,
t: new Date().getTime(),
})
);
}
}
function loadFromFS() {
myConsole.info('reading table from fs');
var content = fileUtils.readFile(fsPath, JSON.parse);
if (!content) {
return;
}
var currentTime = new Date().getTime();
if (currentTime - content.t > samplesExpiration * 1000) {
myConsole.info('RTD samples are too old, discard them, currentTime %s, samplesTime %s', currentTime, content.t);
return;
}
globalSamples = content.d;
myConsole.info('Loaded %s RTD entries from fs ', Object.keys(globalSamples).length);
myConsole.silly('RTD content: %s', JSON.stringify(globalSamples));
}
function _canLivePingBeExecuted(count, timeoutMs, intervalMs, destination) {
if (!interfacesBindingStorage.getMacFromIp(destination) && destination !== "127.0.0.1") {
myConsole.error('LivePing not executed, destination %s not in list of known ip addresses', destination);
return false;
}
if (intervalMs < minLivePingIntervalMs || intervalMs > maxLivePingIntervalMs) {
myConsole.error('LivePing not executed, intervalMs %s out of valid range', intervalMs);
return false;
}
if (count < minLivePingCount || count > maxLivePingCount) {
myConsole.error('LivePing not executed, count %s out of valid range', count);
return false;
}
if (timeoutMs < minLivePingTimeoutMs || timeoutMs > maxLivePingTimeoutMs) {
myConsole.error('LivePing not executed, timeoutMs %s out of valid range', timeoutMs);
return false;
}
return true;
}
function livePing(options, cb) {
var intervalMs = options.interval_ms;
var timeoutMs = options.timeout_ms;
var count = options.count;
var destination = options.destination;
if (!_canLivePingBeExecuted(count, timeoutMs, intervalMs, destination)) {
cb(-1);
return;
}
var pingOpts = {
networkProtocol: ping.NetworkProtocol.IPv4,
packetSize: PING_PACKET_SIZE,
retries: 0,
sessionId: sessionId++,
timeout: timeoutMs,
};
var session = ping.createSession(pingOpts);
for (var i = 0; i < count; i++) {
pingDevice(destination, i * intervalMs, session, function (err, res) {
cb(res);
});
}
}
return {
scanHosts: scanHosts,
storeToFS: storeToFS,
loadFromFS: loadFromFS,
pingDevice: pingDevice,
_pingDeviceBurst: pingDeviceBurst,
livePing: livePing,
_executionCounter: executionCounter,
_globalSamples: globalSamples,
_getGlobalSamples: getGlobalSamples,
};
}
function rtdProcessor(resourceLocator) {
var myConsole = resourceLocator.log.decorateLogs();
var _ = resourceLocator.lodash;
var q = resourceLocator.q;
var engineRequest = resourceLocator.engineRequest;
function percentile(p, list) {
if (p === 0) {
return list[0];
}
var kIndex = Math.ceil(list.length * (p / 100)) - 1;
return list[kIndex] === undefined ? null : list[kIndex];
}
function getMedian(v) {
if (v.length % 2 === 0) {
return (v[v.length / 2 - 1] + v[v.length / 2]) / 2;
} else {
return v[(v.length - 1) / 2];
}
}
function processResults(globalSamples, executionCounter, params) {
if (executionCounter % params.t_report_fast_discoveries !== 0) {
myConsole.info('Skipping ping reporting counter=%s', executionCounter);
return null;
}
var stats = {};
for (var mac in globalSamples) {
if (globalSamples.hasOwnProperty(mac)) {
var sampleList = globalSamples[mac].rtd;
var count = globalSamples[mac].count;
myConsole.info(
'Mac %s sampleList.length %s, globalSamples[mac].count %s, globalSamples[mac].packetLost %s',
mac,
sampleList.length,
count,
globalSamples[mac].packetLost
);
if (count + globalSamples[mac].rtd.length < params.window_size_samples) {
myConsole.info('too few samples, continue');
continue;
}
var lostPackets = globalSamples[mac].packetLost;
var validSamples = sampleList.slice(0);
validSamples.sort(function (a, b) {
return a - b;
});
var qMin = percentile(params.min_q_percentile, validSamples);
var qMax = percentile(params.max_q_percentile, validSamples);
var median;
median = getMedian(validSamples);
stats[mac] = {
qMin: qMin,
median: median,
qMax: qMax,
count: count,
lost: lostPackets,
};
}
}
myConsole.verbose('STATS %s', JSON.stringify(stats));
if (Object.keys(stats).length > 0) {
report(stats)
.then(function () {
for (var mac in stats) {
if (stats.hasOwnProperty(mac)) {
globalSamples[mac].rtd = _.takeRight(globalSamples[mac].rtd, params.previous_window_size);
globalSamples[mac].count = 0;
globalSamples[mac].packetLost = 0;
}
}
})
.catch(function () {
myConsole.verbose('Error reporting RTD stats');
});
}
return stats;
}
var saveToFs = false;
var outFileIndex = 0;
function report(stats) {
if (saveToFs) {
var fs = require('fs');
var prefix = process.env.DOMOTZ_LOG_DIR + '/' + ('0000' + parseInt(outFileIndex)).slice(-4);
var filename = prefix + '_rdt_stats.json';
fs.writeFile(filename, JSON.stringify(stats));
outFileIndex++;
return q.resolve();
} else {
return engineRequest.sendWithPromise('/device/rtd', 'PUT', stats);
}
}
return {
processResults: processResults,
};
}
module.exports.rtdScan = rtdScan;
module.exports.rtdProcessor = rtdProcessor;