UNPKG

domotz-remote-pawn

Version:

Domotz Agent

389 lines (356 loc) 13.8 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 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;