domotz-remote-pawn
Version:
Domotz Agent
389 lines (356 loc) • 13.8 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 Iacopo Papalini <iacopo@domotz.com> on 04/08/2017.
*/
const MODULE = 'UBUNTU CORE NETWORKING';
const MAIN_INTERFACE = 'eth0';
const RESOLV = '/etc/resolv.conf';
const DEFAULT_CONFIGURATION = {
network: {
ethernets: {
eth0: {
addresses: [],
dhcp4: true,
nameservers: {},
},
},
version: 2,
},
};
function Configuration(network, lodash) {
'use strict';
var Netmask = require('netmask').Netmask;
function _netmask(ip, mask) {
return new Netmask(ip + '/' + mask).bitmask;
}
function clone() {
return lodash.cloneDeep(network);
}
function getName(tag) {
return MAIN_INTERFACE + '.' + tag;
}
return {
setStatic: function (ip, mask, gateway, primaryDNS, secondaryDNS) {
var netmask = _netmask(ip, mask);
var address = ip + '/' + netmask;
var network = clone();
network.ethernets = {};
network.ethernets[MAIN_INTERFACE] = {
dhcp4: false,
addresses: [address],
gateway4: gateway,
nameservers: {
addresses: [primaryDNS, secondaryDNS],
},
};
return Configuration(network, lodash);
},
setDHCP: function () {
var network = clone();
network.ethernets = {};
network.ethernets[MAIN_INTERFACE] = {
dhcp4: true,
addresses: [],
nameservers: {},
};
return Configuration(network, lodash);
},
getDHCP: function () {
if (!(MAIN_INTERFACE in network.ethernets)) {
return true;
}
return network.ethernets[MAIN_INTERFACE].dhcp4;
},
addVLAN: function (tag, ip, netmask) {
var network = clone();
if (!('ethernets' in network) || !(MAIN_INTERFACE in network.ethernets)) {
network.ethernets = {};
network.ethernets[MAIN_INTERFACE] = {
dhcp4: true,
addresses: [],
nameservers: {},
};
}
var device = getName(tag);
var address = ip + '/' + _netmask(ip, netmask);
if (typeof network.vlans !== 'object') {
network.vlans = {};
}
network.vlans[device] = {
id: parseInt(tag),
link: MAIN_INTERFACE,
addresses: [address],
};
return Configuration(network, lodash);
},
deleteVLAN: function (tag) {
var network = clone();
delete network.vlans[getName(tag)];
return Configuration(network, lodash);
},
deleteAllVLANs: function () {
var network = clone();
network.vlans = {};
return Configuration(network, lodash);
},
resetNetwork: function () {
return Configuration(DEFAULT_CONFIGURATION.network, lodash);
},
listVLANs: function () {
/**
* returns an array with the VLANS formatted this way:
* [
* {
* "tag": "20",
* "ip_address": "10.0.20.1",
* "netmask": "8"
* },
* {
* "tag": "30",
* "ip_address": "10.0.31.1",
* "netmask": "8"
* }
* ]
*
* @type {Array}
*/
var ret = [];
lodash.forOwn(network.vlans, function (vlan) {
var tmp = vlan.addresses[0].split('/');
var address = tmp[0];
var netmask = tmp[1];
ret.push({
tag: '' + vlan.id,
ip_address: address,
netmask: netmask,
});
});
return ret;
},
data: function () {
return network;
},
};
}
function ConfigurationApplier(ignored, resourceLocator, ipWrapper) {
'use strict';
var lodash = resourceLocator.lodash;
var q = resourceLocator.q;
var ip = resourceLocator.ip;
var myConsole = resourceLocator.log.decorateLogs();
var file = resourceLocator.utils.file;
function wait() {
var deferred = q.defer();
setTimeout(deferred.resolve, 1000);
return deferred.promise;
}
return {
apply: function (configuration, test) {
var mainInterface = _getMainInterfaceConfiguration(configuration);
if (test === undefined) {
test = true;
}
var setNetwork;
var deleteVlans = function () {
return _deleteAllVlans(configuration);
};
var createVlans = function () {
return _createVlans(configuration);
};
if (mainInterface === undefined || mainInterface.dhcp4) {
setNetwork = ipWrapper.activate_dhcp;
} else {
setNetwork = function () {
return setStaticConfiguration(mainInterface, test);
};
}
return ipWrapper.flush().then(setNetwork).then(deleteVlans).then(wait).then(createVlans);
},
};
function _getMainInterfaceConfiguration(configuration) {
var conf = configuration.data();
var ret = conf.ethernets[MAIN_INTERFACE];
if (ret === undefined) {
myConsole.error('Cannot get configuration for main interface');
}
return ret;
}
function setStaticConfiguration(mainInterface, test) {
var myIP = mainInterface.addresses[0];
var gateway = mainInterface.gateway4;
var DNS_0 = mainInterface.nameservers.addresses[0];
var DNS_1 = mainInterface.nameservers.addresses[1];
myConsole.debug('Setting static configuration: %s, gw:%s, dns1: %s, dns1: %s', myIP, gateway, DNS_0, DNS_1);
return ipWrapper
.call('addr', 'add', [myIP, 'dev', MAIN_INTERFACE])
.then(function () {
return ipWrapper.call('route', 'add', ['0.0.0.0/0', 'via', gateway]);
})
.then(function () {
return file.readFile(RESOLV, _replaceDNS(DNS_0, DNS_1), true);
})
.then(function (data) {
myConsole.debug('Writing file %s', RESOLV);
return file.writeFile(RESOLV, data, true);
})
.then(function () {
if (test) {
return ipWrapper.ping(gateway, 1, 1);
}
});
}
function _replaceDNS(DNS_0, DNS_1) {
return function (data) {
data = data.split('\n').filter(function (line) {
return !line.match(/^nameserver.+/);
});
data.push('nameserver ' + DNS_0);
data.push('nameserver ' + DNS_1);
data = data.join('\n');
return data;
};
}
function getVlanNetinfo(address) {
var vlanIp = new ip.Address4(address);
var networkAddress = vlanIp.startAddress().address;
var subnet = vlanIp.subnet;
return { vlanIp: vlanIp.addressMinusSuffix, networkAddress: networkAddress, subnet: subnet };
}
function _deleteAllVlans() {
myConsole.debug('Removing all vlans');
return ipWrapper
.retrieveVlans()
.then(function (vlans) {
var promises = [];
lodash.forOwn(vlans, function (vlan) {
promises.push(
ipWrapper
.call('addr', 'del', [vlan.address, 'dev', vlan.name])
.then(function () {
return ipWrapper.call('addr', 'flush', ['dev', vlan.name]);
})
.then(function () {
return ipWrapper.call('link', 'set', ['dev', vlan.name, 'down']).then(function () {
return ipWrapper.call('link', 'delete', { dev: vlan.name });
});
})
);
});
return q.all(promises);
})
.catch(function (error) {
myConsole.error(error.stack);
throw error;
});
}
function _createVlans(configuration) {
var vlans = configuration.data().vlans || {};
myConsole.debug('Creating VLANS: %s', JSON.stringify(vlans));
var promises = [];
lodash.forOwn(vlans, function (vlan, name) {
var promise = ipWrapper
.call('link', 'add', {
name: name,
link: MAIN_INTERFACE,
type: 'vlan',
id: vlan.id,
})
.then(function () {
var vlanNetInfo = getVlanNetinfo(vlan.addresses[0]);
var deferred = q.defer();
ipWrapper
.call('addr', 'add', [vlanNetInfo.vlanIp + vlanNetInfo.subnet, 'brd', '+', 'dev', name])
.then(function () {
deferred.resolve();
})
.catch(function (error) {
//Ignore 'File exists' errors
if (error.message === 'RTNETLINK answers: File exists') {
myConsole.error('Ignoring address add error when reapplying vlans: ' + error.message);
return deferred.resolve();
}
myConsole.error('Unexpected error applying vlans: ' + error.message);
deferred.reject();
});
return deferred.promise;
})
.then(function () {
return ipWrapper.call('link', 'set', ['dev', name, 'up']);
})
.catch(function (error) {
myConsole.error(error.stack);
throw error;
});
promises.push(promise);
});
return q.all(promises);
}
}
function ConfigurationFileManager(resourceLocator) {
'use strict';
var myConsole = resourceLocator.log.decorateLogs().decorate(MODULE + ' ConfigurationFileManager - ');
var path = resourceLocator.path;
var YAML = require('yamljs');
var file = resourceLocator.utils.file;
var lodash = resourceLocator.lodash;
var interfacesFile = path.join('/etc', 'netplan', '00-snapd-config.yaml');
var interfacesBackupFile = path.join('/etc', 'netplan', '00-snapd-config.bak');
myConsole.info('Network config file: %s', interfacesFile);
function loadConfiguration() {
/**
* Synchronously loads the network configuration
*/
var domotzConfiguration;
try {
myConsole.info('Recovering network configuration from file %s', interfacesFile);
domotzConfiguration = YAML.parseFile(interfacesFile);
} catch (error) {
myConsole.warn('Cannot parse custom interfaces file: %s', error.message);
domotzConfiguration = DEFAULT_CONFIGURATION;
}
var network = domotzConfiguration.network;
var ret = Configuration(network, lodash);
myConsole.debug('Configuration: ' + JSON.stringify(ret.data()));
return ret;
}
function backupConfiguration() {
myConsole.debug('Backup configuration file %s', interfacesBackupFile);
return file.touchFile(interfacesFile).then(function () {
return file.copyFileAsync(interfacesFile, interfacesBackupFile);
});
}
function restoreConfiguration() {
return file.copyFileAsync(interfacesBackupFile, interfacesFile).then(deleteBackup);
}
function deleteBackup() {
return file.deleteFile(interfacesBackupFile, true);
}
function writeConfiguration(newConfiguration) {
myConsole.info('Updating %s configuration file', interfacesFile);
return file.writeFile(interfacesFile, YAML.stringify({ network: newConfiguration.data() }, 10), true);
}
return {
deleteBackup: deleteBackup,
restoreConfiguration: restoreConfiguration,
backupConfiguration: backupConfiguration,
loadConfiguration: loadConfiguration,
writeConfiguration: writeConfiguration,
};
}
module.exports.applier = ConfigurationApplier;
module.exports.configurationManager = ConfigurationFileManager;
module.exports._configuration = Configuration;