domotz-remote-pawn
Version:
Domotz Agent
382 lines (336 loc) • 13.1 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/>.
**/
/**
* InterfacesBinding provides functionality to project the network
* discovery onto a map of mac - ip tuple, hereafter called as "i_binding"
*
* This class is stateful (remembers the binding discovered at each iteration).
*
* Created by fausto on 18/05/16.
*/
module.exports.factory = function (q, util, fileUtils, engineRequest, hostInfo, macAddressesMapFile) {
"use strict";
macAddressesMapFile = macAddressesMapFile || process.env.DOMOTZ_CONF_DIR + '/device_map.conf';
console.log('BINDINGS - Initialize device map from: %s', macAddressesMapFile);
function cloneStateRecord(ibinding) {
return {
'mac': ibinding.mac,
'ip': ibinding.ip,
'hostname': ibinding.hostname,
'state': ibinding.state
};
}
function createStateRecord(mac, ip, hostname, state) {
return {
'mac': mac,
'ip': ip,
'hostname': hostname,
'state': state
};
}
function createReportRecord(ibinding) {
return {
'mac': ibinding.mac,
'ip': ibinding.ip,
'hostname': ibinding.hostname
};
}
function getDevice(ip, mac, hostname) {
if (mac) {
deviceMap[ip] = mac;
}
return {
ip: ip,
mac: deviceMap[ip] || createCustomMacFromIp(ip),
hostname: (hostname === "\n") ? "" : hostname
};
}
function deviceProjection(device) {
return {
ip: device.ip,
host_name: device.hostname
};
}
function asBindingId(mac, ip) {
return '(' + mac + " - " + ip + ')';
}
function bindingToId(ibinding) {
return asBindingId(ibinding.mac, ibinding.ip);
}
function setState(mac, ip, hostname, state) {
var bindingId = asBindingId(mac, ip);
iBindingsCurrentSnapshot[bindingId] = createStateRecord(mac, ip, hostname, state);
}
/**
* ------------------- CURRENT AND PREVIOUS SNAPSHOT -----------------------
*
* Current and Previous Snapshot of discovery
* {
* <mac_address> : <ip_address>
* }
*/
var iBindingsCurrentSnapshot = {};
var iBindingsPreviousSnapshot = {};
var counterCycle = 0;
function cleanAll() {
iBindingsCurrentSnapshot = {};
iBindingsPreviousSnapshot = {};
}
function setFresh(mac, ip, hostname) {
console.verbose("BINDINGS - setFresh called with bindings " + asBindingId(mac, ip) + " (icycle: %s) ", counterCycle);
setState(mac, ip, hostname, "FRESH");
}
function foundInPreviousSnapshot(fresh_binding) {
var bid = bindingToId(fresh_binding);
return iBindingsPreviousSnapshot[bid] !== undefined;
}
function listByState(state) {
var filteredBindingList = [];
for (var binding_id in iBindingsCurrentSnapshot) {
if (iBindingsCurrentSnapshot.hasOwnProperty(binding_id)) {
if (iBindingsCurrentSnapshot[binding_id].state === state) {
var record = createReportRecord(iBindingsCurrentSnapshot[binding_id]);
filteredBindingList.push(record);
}
}
}
return filteredBindingList;
}
function listNew() {
console.verbose("BINDINGS - listNew called (icycle: %s ) ...", counterCycle);
var newBindings = [];
var currently_fresh_bindings = listFresh();
for (var i = 0; i < currently_fresh_bindings.length; i++) {
if (!foundInPreviousSnapshot(currently_fresh_bindings[i])) {
var record = createReportRecord(currently_fresh_bindings[i]);
newBindings.push(record);
}
}
for (var j = 0; j < newBindings.length; j++) {
console.info("BINDINGS - Found new binding: %s (icycle: %s)",
JSON.stringify(newBindings[j]), counterCycle);
}
return newBindings;
}
function listFreshByMac(mac) {
var freshByMacList = [];
var freshList = listFresh();
for(var i=0; i<freshList.length; i++){
var dev = freshList[i];
if (dev.mac === mac){
freshByMacList.push(dev);
}
}
return freshByMacList;
}
function listFresh() {
return listByState("FRESH");
}
function listStale() {
return listByState("STALE");
}
function staleAll() {
console.verbose("BINDINGS - All bindings set to stale (icycle: %s)", counterCycle);
iBindingsPreviousSnapshot = {};
for (var bindingID in iBindingsCurrentSnapshot) {
if (iBindingsCurrentSnapshot.hasOwnProperty(bindingID)) {
// Copy i_bindings to 'when at last staleAll call' i_bindings state for later use
iBindingsPreviousSnapshot[bindingID] = cloneStateRecord(iBindingsCurrentSnapshot[bindingID]);
iBindingsCurrentSnapshot[bindingID].state = "STALE";
}
}
}
function purgeStale() {
if(counterCycle > 100000){
counterCycle = 0;
}
counterCycle += 1;
console.verbose("BINDINGS - Purge Stale called. Generate new icycle %s ", counterCycle);
setInterfaceInfo();
var staleBindings = listStale();
var toRemove = [];
for (var i = 0; i < staleBindings.length; i++) {
toRemove.push(bindingToId(staleBindings[i]));
}
for (var j = 0; j < toRemove.length; j++) {
console.verbose("BINDINGS - purging binding: %s (icycle: %s)",
JSON.stringify(toRemove[j]), counterCycle);
delete iBindingsCurrentSnapshot[toRemove[j]];
}
}
function rollback() {
console.info("BINDINGS - Rollback was called. State before rollback: %s",
JSON.stringify(iBindingsCurrentSnapshot));
iBindingsCurrentSnapshot = {};
for (var binding_id in iBindingsPreviousSnapshot) {
if (iBindingsPreviousSnapshot.hasOwnProperty(binding_id)) {
// Copy back ibindings from 'when at last staleAll call' to ibindings state
iBindingsCurrentSnapshot[binding_id] = cloneStateRecord(iBindingsPreviousSnapshot[binding_id]);
}
}
console.debug("BINDINGS - State after rollback: %s", JSON.stringify(iBindingsCurrentSnapshot));
}
/**
* ---------------------------- INITIALIZER --------------------------------
*
* It assures that the current status cache is as the last sent to the
* engine.
*
*/
var initialized = false;
function assureInitialized() {
if (initialized) {
return q.resolve(null);
}
console.info("BINDINGS - INITIALIZING Devices list " +
"(sending request to Engine) ....");
return engineRequest.sendWithPromise('/device', 'GET', {}, function(response){
console.verbose("GOT RESPONSE, analyzing ... ");
for (var i=0; i < response.body.length; i++){
var device = response.body[i];
var mac = device.mac,
hostname = device.hostname,
status = device.status;
if (status === "ONLINE") {
for(var j=0; j< device.ip_addresses.length; j++){
var ip = device.ip_addresses[j];
deviceMap[ip] = mac;
console.debug("BINDINGS - Device mac: %s. IP: %s. " +
"Hostname: %s -- is %s, adding to list of " +
"previously known devices ..", mac,
ip, hostname, status);
setState(mac, ip, hostname, "FRESH");
}
}
else {
console.verbose("BINDINGS - Device mac: %s ip: %s hostname: %s " +
"-- is %s", mac, device.ip, hostname, status);
}
}
initialized = true;
});
}
/**
* ---------------------------- INITIALIZER --------------------------------
*/
/**
* ---------------------------- FILE_SYSTEM --------------------------------
*/
function save(fileToSave) {
console.info("BINDINGS - InterfaceBinding persisting state to file: %s", fileToSave);
fileUtils.writeFileSafe(fileToSave, JSON.stringify(iBindingsCurrentSnapshot, null, 2));
}
function load(fileToRead) {
console.info("BINDINGS - InterfaceBinding reloading state from file: %s", fileToRead);
cleanAll();
iBindingsCurrentSnapshot = fileUtils.readFile(fileToRead, JSON.parse);
}
/**
* ---------------------------- FILE_SYSTEM --------------------------------
*/
/**
* ------------------- PERSISTENCE INTERFACE INFO --------------------------
*
* It's an object that contains the correspondence between mac address and
* ip address. It is exploited by a lot of services that need an IP address
* to reach the device from the MAC.
* {
* <mac_address> : <ip_address>
* }
*/
var persistentInterfaceInfo = {};
function setInterfaceInfo(){
persistentInterfaceInfo = {};
for (var interface_index in iBindingsCurrentSnapshot) {
if (iBindingsCurrentSnapshot.hasOwnProperty(interface_index)) {
var interface_info = iBindingsCurrentSnapshot[interface_index];
if (persistentInterfaceInfo[interface_info.mac] === undefined){
persistentInterfaceInfo[interface_info.mac] = interface_info.ip;
}
}
}
}
function getInterfaceInfo(){
return persistentInterfaceInfo;
}
/**
* ------------------- PERSISTENCE INTERFACE INFO --------------------------
*/
/**
* ----------------------- DEVICE REVERSE MAP ------------------------------
* For layer-3 monitoring we keep a reverse mapping IP -> MAC address
* initialized from a file:
* <ip> <mac>
*/
function _toHexWith2Digits(byte) {
return ("0" + (parseInt(byte)).toString(16)).slice(-2);
}
var deviceMap = fileUtils.readFile(macAddressesMapFile, function(content) {
var rows = content.split('\n');
var tmp = {};
for (var i = 0; i < rows.length; i++) {
var row = rows[i],
fields = row.split(/\s+/);
if (row && fields) {
var ip = fields[0], mac = fields[1];
tmp[ip] = mac;
console.info('BINDINGS - Add mapping %s --> %s for custom external IPs', ip, mac);
}
}
return tmp;
}) || {};
/**
* ID network + last 16 bits are unique ID (netmask is max 22...)
* N.B. The interface exists because we have just scanned it
*/
function createCustomMacFromIp(ip) {
if (!hostInfo.getInterfaceType(ip)) { // true = custom, false = real
return null;
}
var bytes = ip.split('.');
var ipB0 = _toHexWith2Digits(bytes[0]);
var ipB1 = _toHexWith2Digits(bytes[1]);
var ipB2 = _toHexWith2Digits(bytes[2]);
var ipB3 = _toHexWith2Digits(bytes[3]);
var mac = util.format('CS:TM:%s:%s:%s:%s', ipB0, ipB1, ipB2, ipB3).toUpperCase();
console.info('BINDINGS - Create unique ID fo ip %s - byte ' +
'from ip: %s.%s.%s.%s Caching [%s] --> [%s].',
ip, ipB0, ipB1, ipB2, ipB3, ip, mac);
deviceMap[ip] = mac;
return mac;
}
/**
* ----------------------- DEVICE REVERSE MAP ------------------------------
*/
return {
staleAll: staleAll,
cleanAll: cleanAll,
setFresh: setFresh,
listFresh: listFresh,
listFreshByMac: listFreshByMac,
listNew: listNew,
listStale: listStale,
purgeStale: purgeStale,
getDevice: getDevice,
deviceProjection: deviceProjection,
assureInitialized: assureInitialized,
getInterfaceInfo: getInterfaceInfo,
save: save,
load: load,
rollback: rollback
};
};