UNPKG

domotz-remote-pawn

Version:

Domotz Agent

426 lines (362 loc) 15.5 kB
/** 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. */ 'use strict'; 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;