View file File name : configmanager.py Content :#!/usr/bin/python3 # # Copyright (C) 2018 Canonical, Ltd. # Author: Mathieu Trudel-Lapierre <mathieu.trudel-lapierre@canonical.com> # # This program 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; version 3. # # This program 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 this program. If not, see <http://www.gnu.org/licenses/>. '''netplan configuration manager''' import glob import logging import os import shutil import sys import tempfile import yaml class ConfigManager(object): def __init__(self, prefix="/", extra_files={}): self.prefix = prefix self.tempdir = tempfile.mkdtemp(prefix='netplan_') self.temp_etc = os.path.join(self.tempdir, "etc") self.temp_run = os.path.join(self.tempdir, "run") self.extra_files = extra_files self.config = {} self.new_interfaces = set() @property def network(self): return self.config['network'] @property def interfaces(self): interfaces = {} interfaces.update(self.ovs_ports) interfaces.update(self.ethernets) interfaces.update(self.modems) interfaces.update(self.wifis) interfaces.update(self.bridges) interfaces.update(self.bonds) interfaces.update(self.tunnels) interfaces.update(self.vlans) return interfaces @property def physical_interfaces(self): interfaces = {} interfaces.update(self.ethernets) interfaces.update(self.modems) interfaces.update(self.wifis) return interfaces @property def virtual_interfaces(self): interfaces = {} # what about ovs_ports? interfaces.update(self.bridges) interfaces.update(self.bonds) interfaces.update(self.tunnels) interfaces.update(self.vlans) return interfaces @property def ovs_ports(self): return self.network['ovs_ports'] @property def openvswitch(self): return self.network['openvswitch'] @property def ethernets(self): return self.network['ethernets'] @property def modems(self): return self.network['modems'] @property def wifis(self): return self.network['wifis'] @property def bridges(self): return self.network['bridges'] @property def bonds(self): return self.network['bonds'] @property def tunnels(self): return self.network['tunnels'] @property def vlans(self): return self.network['vlans'] @property def nm_devices(self): return self.network['nm-devices'] @property def version(self): return self.network['version'] @property def renderer(self): return self.network['renderer'] @property def tree(self): return self.strip_tree(self.config) @staticmethod def strip_tree(data): '''clear empty branches''' new_data = {} for k, v in data.items(): if isinstance(v, dict): v = ConfigManager.strip_tree(v) if v not in (u'', None, {}): new_data[k] = v return new_data def parse(self, extra_config=[]): """ Parse all our config files to return an object that describes the system's entire configuration, so that it can later be interrogated. Returns a dict that contains the entire, collated and merged YAML. """ # TODO: Clean this up, there's no solid reason why we should parse YAML # in two different spots; here and in parse.c. We'd do better by # parsing things once, in C form, and having some small glue # Cpython code to call on the right methods and return an object # that is meaningful for the Python code; but minimal parsing in # pure Python will do for now. ~cyphermox # /run/netplan shadows /etc/netplan/, which shadows /lib/netplan names_to_paths = {} for yaml_dir in ['lib', 'etc', 'run']: for yaml_file in glob.glob(os.path.join(self.prefix, yaml_dir, 'netplan', '*.yaml')): names_to_paths[os.path.basename(yaml_file)] = yaml_file files = [names_to_paths[name] for name in sorted(names_to_paths.keys())] self.config['network'] = { 'ovs_ports': {}, 'openvswitch': {}, 'ethernets': {}, 'modems': {}, 'wifis': {}, 'bridges': {}, 'bonds': {}, 'tunnels': {}, 'vlans': {}, 'nm-devices': {}, 'version': None, 'renderer': None } for yaml_file in files: self._merge_yaml_config(yaml_file) for yaml_file in extra_config: self.new_interfaces |= self._merge_yaml_config(yaml_file) logging.debug("Merged config:\n{}".format(yaml.dump(self.tree, default_flow_style=False))) def add(self, config_dict): for config_file in config_dict: self._copy_file(config_file, config_dict[config_file]) self.extra_files.update(config_dict) def backup(self, backup_config_dir=True): if backup_config_dir: self._copy_tree(os.path.join(self.prefix, "etc/netplan"), os.path.join(self.temp_etc, "netplan")) self._copy_tree(os.path.join(self.prefix, "run/NetworkManager/system-connections"), os.path.join(self.temp_run, "NetworkManager", "system-connections"), missing_ok=True) self._copy_tree(os.path.join(self.prefix, "run/systemd/network"), os.path.join(self.temp_run, "systemd", "network"), missing_ok=True) def revert(self): try: for extra_file in dict(self.extra_files): os.unlink(self.extra_files[extra_file]) del self.extra_files[extra_file] temp_nm_path = "{}/NetworkManager/system-connections".format(self.temp_run) temp_networkd_path = "{}/systemd/network".format(self.temp_run) if os.path.exists(temp_nm_path): shutil.rmtree(os.path.join(self.prefix, "run/NetworkManager/system-connections")) self._copy_tree(temp_nm_path, os.path.join(self.prefix, "run/NetworkManager/system-connections")) if os.path.exists(temp_networkd_path): shutil.rmtree(os.path.join(self.prefix, "run/systemd/network")) self._copy_tree(temp_networkd_path, os.path.join(self.prefix, "run/systemd/network")) except Exception as e: # pragma: nocover (only relevant to filesystem failures) # If we reach here, we're in big trouble. We may have wiped out # file NM or networkd are using, and we most likely removed the # "new" config -- or at least our copy of it. # Given that we're in some halfway done revert; warn the user # aggressively and drop everything; leaving any remaining backups # around for the user to handle themselves. logging.error("Something really bad happened while reverting config: {}".format(e)) logging.error("You should verify the netplan YAML in /etc/netplan and probably run 'netplan apply' again.") sys.exit(-1) def cleanup(self): shutil.rmtree(self.tempdir) def __del__(self): try: self.cleanup() except FileNotFoundError: # If cleanup() was called before, there is nothing to delete pass def _copy_file(self, src, dst): shutil.copy(src, dst) def _copy_tree(self, src, dst, missing_ok=False): try: shutil.copytree(src, dst) except FileNotFoundError: if missing_ok: pass else: raise def _merge_ovs_ports_config(self, orig, new): new_interfaces = set() ports = dict() if 'ports' in new: for p1, p2 in new.get('ports'): # Spoof an interface config for patch ports, which are usually # just strings. Add 'peer' and mark it via 'openvswitch' key. ports[p1] = {'peer': p2, 'openvswitch': {}} ports[p2] = {'peer': p1, 'openvswitch': {}} changed_ifaces = list(ports.keys()) for ifname in changed_ifaces: iface = ports.pop(ifname) if ifname in orig: logging.debug("{} exists in {}".format(ifname, orig)) orig[ifname].update(iface) else: logging.debug("{} not found in {}".format(ifname, orig)) orig[ifname] = iface new_interfaces.add(ifname) return new_interfaces def _merge_interface_config(self, orig, new): new_interfaces = set() changed_ifaces = list(new.keys()) for ifname in changed_ifaces: iface = new.pop(ifname) if ifname in orig: logging.debug("{} exists in {}".format(ifname, orig)) orig[ifname].update(iface) else: logging.debug("{} not found in {}".format(ifname, orig)) orig[ifname] = iface new_interfaces.add(ifname) return new_interfaces def _merge_yaml_config(self, yaml_file): new_interfaces = set() try: with open(yaml_file) as f: yaml_data = yaml.load(f, Loader=yaml.CSafeLoader) network = None if yaml_data is not None: network = yaml_data.get('network') if network: if 'openvswitch' in network: new = self._merge_ovs_ports_config(self.ovs_ports, network.get('openvswitch')) new_interfaces |= new self.network['openvswitch'] = network.get('openvswitch') if 'ethernets' in network: new = self._merge_interface_config(self.ethernets, network.get('ethernets')) new_interfaces |= new if 'modems' in network: new = self._merge_interface_config(self.modems, network.get('modems')) new_interfaces |= new if 'wifis' in network: new = self._merge_interface_config(self.wifis, network.get('wifis')) new_interfaces |= new if 'bridges' in network: new = self._merge_interface_config(self.bridges, network.get('bridges')) new_interfaces |= new if 'bonds' in network: new = self._merge_interface_config(self.bonds, network.get('bonds')) new_interfaces |= new if 'tunnels' in network: new = self._merge_interface_config(self.tunnels, network.get('tunnels')) new_interfaces |= new if 'vlans' in network: new = self._merge_interface_config(self.vlans, network.get('vlans')) new_interfaces |= new if 'nm-devices' in network: new = self._merge_interface_config(self.nm_devices, network.get('nm-devices')) new_interfaces |= new if 'version' in network: self.network['version'] = network.get('version') if 'renderer' in network: self.network['renderer'] = network.get('renderer') return new_interfaces except (IOError, yaml.YAMLError): # pragma: nocover (filesystem failures/invalid YAML) logging.error('Error while loading {}, aborting.'.format(yaml_file)) sys.exit(1) class ConfigurationError(Exception): """ Configuration could not be parsed or has otherwise failed to apply """ pass