def command_get(self): config_manager = ConfigManager(prefix=self.root_dir) config_manager.parse() tree = config_manager.tree if self.key != 'all': # The 'network.' prefix is optional for netsted keys, its always assumed to be there if not self.key.startswith( 'network.') and not self.key == 'network': self.key = 'network.' + self.key # Split at '.' but not at '\.' via negative lookbehind expression for k in re.split(r'(?<!\\)\.', self.key): k = k.replace('\\.', '.') # Unescape interface-ids, containing dots if k in tree.keys(): tree = tree[k] if not isinstance(tree, dict): break else: tree = None break out = yaml.dump(tree, default_flow_style=False)[:-1] # Remove trailing '\n' if not isinstance(tree, dict) and not isinstance(tree, list): out = out[:-4] # Remove yaml.dump's '\n...' on primitive values print(out)
class NetplanTry(utils.NetplanCommand): def __init__(self): super().__init__(command_id='try', description='Try to apply a new netplan config to running ' 'system, with automatic rollback', leaf=True) self.configuration_changed = False self.new_interfaces = None self.config_manager = ConfigManager() def run(self): # pragma: nocover (requires user input) self.parser.add_argument('--config-file', help='Apply the config file in argument in addition to current configuration.') self.parser.add_argument('--timeout', type=int, default=DEFAULT_INPUT_TIMEOUT, help="Maximum number of seconds to wait for the user's confirmation") self.func = self.command_try self.parse_args() self.run_command() def command_try(self): # pragma: nocover (requires user input) if not self.is_revertable(): sys.exit(os.EX_CONFIG) try: fd = sys.stdin.fileno() t = netplan.terminal.Terminal(fd) # we really don't want to be interrupted while doing backup/revert operations signal.signal(signal.SIGINT, self._signal_handler) self.backup() self.setup() NetplanApply.command_apply(run_generate=True, sync=True, exit_on_error=False) t.get_confirmation_input(timeout=self.timeout) except netplan.terminal.InputRejected: print("\nReverting.") self.revert() except netplan.terminal.InputAccepted: print("\nConfiguration accepted.") except Exception as e: print("\nAn error occured: %s" % e) print("\nReverting.") self.revert() finally: self.cleanup() def backup(self): # pragma: nocover (requires user input) backup_config_dir = False if self.config_file: backup_config_dir = True self.config_manager.backup(backup_config_dir=backup_config_dir) def setup(self): # pragma: nocover (requires user input) if self.config_file: dest_dir = os.path.join("/", "etc", "netplan") dest_name = os.path.basename(self.config_file).rstrip('.yaml') dest_suffix = time.time() dest_path = os.path.join(dest_dir, "{}.{}.yaml".format(dest_name, dest_suffix)) self.config_manager.add({self.config_file: dest_path}) self.configuration_changed = True def revert(self): # pragma: nocover (requires user input) self.config_manager.revert() NetplanApply.command_apply(run_generate=False, sync=True, exit_on_error=False) for ifname in self.new_interfaces: if ifname not in self.config_manager.bonds and \ ifname not in self.config_manager.bridges and \ ifname not in self.config_manager.vlans: logging.debug("{} will not be removed: not a virtual interface".format(ifname)) continue try: cmd = ['ip', 'link', 'del', ifname] subprocess.check_call(cmd) except subprocess.CalledProcessError: logging.warn("Could not revert (remove) new interface '{}'".format(ifname)) def cleanup(self): # pragma: nocover (requires user input) self.config_manager.cleanup() def is_revertable(self): # pragma: nocover (requires user input) ''' Check if the configuration is revertable, if it doesn't contain bits that we know are likely to render the system unstable if we apply it, or if we revert. Returns True if the parsed config is "revertable", meaning that we can actually rely on backends to re-apply /all/ of the relevant configuration to interfaces when their config changes. Returns False if the parsed config contains options that are known to not cleanly revert via the backend. ''' # Parse; including any new config file passed on the command-line: # new config might include things we can't revert. extra_config = [] if self.config_file: extra_config.append(self.config_file) self.config_manager.parse(extra_config=extra_config) self.new_interfaces = self.config_manager.new_interfaces logging.debug("New interfaces: {}".format(self.new_interfaces)) revert_unsupported = [] # Bridges and bonds are special. They typically include (or could include) # more than one device in them, and they can be set with special parameters # to tweak their behavior, which are really hard to "revert", especially # as systemd-networkd doesn't necessarily touch them when config changes. multi_iface = {} multi_iface.update(self.config_manager.bridges) multi_iface.update(self.config_manager.bonds) for ifname, settings in multi_iface.items(): if settings and 'parameters' in settings: reason = "reverting custom parameters for bridges and bonds is not supported" revert_unsupported.append((ifname, reason)) if revert_unsupported: for ifname, reason in revert_unsupported: print("{}: {}".format(ifname, reason)) print("\nPlease carefully review the configuration and use 'netplan apply' directly.") return False return True def _signal_handler(self, signal, frame): # pragma: nocover (requires user input) if self.configuration_changed: raise netplan.terminal.InputRejected()
def command_apply( run_generate=True, sync=False, exit_on_error=True): # pragma: nocover (covered in autopkgtest) # if we are inside a snap, then call dbus to run netplan apply instead if "SNAP" in os.environ: # TODO: maybe check if we are inside a classic snap and don't do # this if we are in a classic snap? busctl = shutil.which("busctl") if busctl is None: raise RuntimeError("missing busctl utility") res = subprocess.call([ busctl, "call", "--quiet", "--system", "io.netplan.Netplan", # the service "/io/netplan/Netplan", # the object "io.netplan.Netplan", # the interface "Apply", # the method ]) if res != 0: if exit_on_error: sys.exit(res) elif res == 130: raise PermissionError( "failed to communicate with dbus service") elif res == 1: raise RuntimeError( "failed to communicate with dbus service") else: return old_files_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) old_files_nm = bool( glob.glob('/run/NetworkManager/system-connections/netplan-*')) generator_call = [] generate_out = None if 'NETPLAN_PROFILE' in os.environ: generator_call.extend(['valgrind', '--leak-check=full']) generate_out = subprocess.STDOUT generator_call.append(utils.get_generator_path()) if run_generate and subprocess.call(generator_call, stderr=generate_out) != 0: if exit_on_error: sys.exit(os.EX_CONFIG) else: raise ConfigurationError( "the configuration could not be generated") config_manager = ConfigManager() devices = netifaces.interfaces() # Re-start service when # 1. We have configuration files for it # 2. Previously we had config files for it but not anymore # Ideally we should compare the content of the *netplan-* files before and # after generation to minimize the number of re-starts, but the conditions # above works too. restart_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) if not restart_networkd and old_files_networkd: restart_networkd = True restart_nm = bool( glob.glob('/run/NetworkManager/system-connections/netplan-*')) if not restart_nm and old_files_nm: restart_nm = True # stop backends if restart_networkd: logging.debug( 'netplan generated networkd configuration changed, restarting networkd' ) utils.systemctl_networkd('stop', sync=sync, extra_services=['netplan-wpa@*.service']) else: logging.debug('no netplan generated networkd configuration exists') if restart_nm: logging.debug( 'netplan generated NM configuration changed, restarting NM') if utils.nm_running(): # restarting NM does not cause new config to be applied, need to shut down devices first for device in devices: # ignore failures here -- some/many devices might not be managed by NM try: utils.nmcli(['device', 'disconnect', device]) except subprocess.CalledProcessError: pass utils.systemctl_network_manager('stop', sync=sync) else: logging.debug('no netplan generated NM configuration exists') # Refresh devices now; restarting a backend might have made something appear. devices = netifaces.interfaces() # evaluate config for extra steps we need to take (like renaming) # for now, only applies to non-virtual (real) devices. config_manager.parse() changes = NetplanApply.process_link_changes(devices, config_manager) # if the interface is up, we can still apply some .link file changes devices = netifaces.interfaces() for device in devices: logging.debug('netplan triggering .link rules for %s', device) try: subprocess.check_call([ 'udevadm', 'test-builtin', 'net_setup_link', '/sys/class/net/' + device ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: logging.debug('Ignoring device without syspath: %s', device) # apply renames to "down" devices for iface, settings in changes.items(): if settings.get('name'): subprocess.check_call([ 'ip', 'link', 'set', 'dev', iface, 'name', settings.get('name') ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.check_call(['udevadm', 'settle']) # (re)start backends if restart_networkd: netplan_wpa = [ os.path.basename(f) for f in glob.glob( '/run/systemd/system/*.wants/netplan-wpa@*.service') ] utils.systemctl_networkd('start', sync=sync, extra_services=netplan_wpa) if restart_nm: utils.systemctl_network_manager('start', sync=sync)
def command_apply(self, run_generate=True, sync=False, exit_on_error=True): # pragma: nocover (covered in autopkgtest) config_manager = ConfigManager() # For certain use-cases, we might want to only apply specific configuration. # If we only need SR-IOV configuration, do that and exit early. if self.sriov_only: NetplanApply.process_sriov_config(config_manager, exit_on_error) return # If we only need OpenVSwitch cleanup, do that and exit early. elif self.only_ovs_cleanup: NetplanApply.process_ovs_cleanup(config_manager, False, False, exit_on_error) return # if we are inside a snap, then call dbus to run netplan apply instead if "SNAP" in os.environ: # TODO: maybe check if we are inside a classic snap and don't do # this if we are in a classic snap? busctl = shutil.which("busctl") if busctl is None: raise RuntimeError("missing busctl utility") # XXX: DO NOT TOUCH or change this API call, it is used by snapd to communicate # using core20 netplan binary/client/CLI on core18 base systems. Any change # must be agreed upon with the snapd team, so we don't break support for # base systems running older netplan versions. res = subprocess.call([busctl, "call", "--quiet", "--system", "io.netplan.Netplan", # the service "/io/netplan/Netplan", # the object "io.netplan.Netplan", # the interface "Apply", # the method ]) if res != 0: if exit_on_error: sys.exit(res) elif res == 130: raise PermissionError( "failed to communicate with dbus service") elif res == 1: raise RuntimeError( "failed to communicate with dbus service") else: return ovs_cleanup_service = '/run/systemd/system/netplan-ovs-cleanup.service' old_files_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) old_ovs_glob = glob.glob('/run/systemd/system/netplan-ovs-*') # Ignore netplan-ovs-cleanup.service, as it can always be there if ovs_cleanup_service in old_ovs_glob: old_ovs_glob.remove(ovs_cleanup_service) old_files_ovs = bool(old_ovs_glob) old_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*') nm_ifaces = utils.nm_interfaces(old_nm_glob, netifaces.interfaces()) old_files_nm = bool(old_nm_glob) generator_call = [] generate_out = None if 'NETPLAN_PROFILE' in os.environ: generator_call.extend(['valgrind', '--leak-check=full']) generate_out = subprocess.STDOUT generator_call.append(utils.get_generator_path()) if run_generate and subprocess.call(generator_call, stderr=generate_out) != 0: if exit_on_error: sys.exit(os.EX_CONFIG) else: raise ConfigurationError("the configuration could not be generated") devices = netifaces.interfaces() # Re-start service when # 1. We have configuration files for it # 2. Previously we had config files for it but not anymore # Ideally we should compare the content of the *netplan-* files before and # after generation to minimize the number of re-starts, but the conditions # above works too. restart_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) if not restart_networkd and old_files_networkd: restart_networkd = True restart_ovs_glob = glob.glob('/run/systemd/system/netplan-ovs-*') # Ignore netplan-ovs-cleanup.service, as it can always be there if ovs_cleanup_service in restart_ovs_glob: restart_ovs_glob.remove(ovs_cleanup_service) restart_ovs = bool(restart_ovs_glob) if not restart_ovs and old_files_ovs: # OVS is managed via systemd units restart_networkd = True restart_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*') nm_ifaces.update(utils.nm_interfaces(restart_nm_glob, devices)) restart_nm = bool(restart_nm_glob) if not restart_nm and old_files_nm: restart_nm = True # stop backends if restart_networkd: logging.debug('netplan generated networkd configuration changed, reloading networkd') # Running 'systemctl daemon-reload' will re-run the netplan systemd generator, # so let's make sure we only run it iff we're willing to run 'netplan generate' if run_generate: utils.systemctl_daemon_reload() # Clean up any old netplan related OVS ports/bonds/bridges, if applicable NetplanApply.process_ovs_cleanup(config_manager, old_files_ovs, restart_ovs, exit_on_error) wpa_services = ['netplan-wpa-*.service'] # Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an # upgraded system, we need to make sure to stop those. if utils.systemctl_is_active('netplan-wpa@*.service'): wpa_services.insert(0, 'netplan-wpa@*.service') utils.systemctl('stop', wpa_services, sync=sync) else: logging.debug('no netplan generated networkd configuration exists') if restart_nm: logging.debug('netplan generated NM configuration changed, restarting NM') if utils.nm_running(): # restarting NM does not cause new config to be applied, need to shut down devices first for device in devices: if device not in nm_ifaces: continue # do not touch this interface # ignore failures here -- some/many devices might not be managed by NM try: utils.nmcli(['device', 'disconnect', device]) except subprocess.CalledProcessError: pass utils.systemctl_network_manager('stop', sync=sync) else: logging.debug('no netplan generated NM configuration exists') # Refresh devices now; restarting a backend might have made something appear. devices = netifaces.interfaces() # evaluate config for extra steps we need to take (like renaming) # for now, only applies to non-virtual (real) devices. config_manager.parse() changes = NetplanApply.process_link_changes(devices, config_manager) # if the interface is up, we can still apply some .link file changes # but we cannot apply the interface rename via udev, as it won't touch # the interface name, if it was already renamed once (e.g. during boot), # because of the NamePolicy=keep default: # https://www.freedesktop.org/software/systemd/man/systemd.net-naming-scheme.html devices = netifaces.interfaces() for device in devices: logging.debug('netplan triggering .link rules for %s', device) try: subprocess.check_call(['udevadm', 'test-builtin', 'net_setup_link', '/sys/class/net/' + device], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: logging.debug('Ignoring device without syspath: %s', device) # apply some more changes manually for iface, settings in changes.items(): # rename non-critical network interfaces if settings.get('name'): # bring down the interface, using its current (matched) interface name subprocess.check_call(['ip', 'link', 'set', 'dev', iface, 'down'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # rename the interface to the name given via 'set-name' subprocess.check_call(['ip', 'link', 'set', 'dev', iface, 'name', settings.get('name')], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.check_call(['udevadm', 'settle']) # apply any SR-IOV related changes, if applicable NetplanApply.process_sriov_config(config_manager, exit_on_error) # (re)start backends if restart_networkd: netplan_wpa = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-wpa-*.service')] # exclude the special 'netplan-ovs-cleanup.service' unit netplan_ovs = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-ovs-*.service') if not f.endswith('/' + OVS_CLEANUP_SERVICE)] # Run 'systemctl start' command synchronously, to avoid race conditions # with 'oneshot' systemd service units, e.g. netplan-ovs-*.service. utils.networkctl_reconfigure(utils.networkd_interfaces()) # 1st: execute OVS cleanup, to avoid races while applying OVS config utils.systemctl('start', [OVS_CLEANUP_SERVICE], sync=True) # 2nd: start all other services utils.systemctl('start', netplan_wpa + netplan_ovs, sync=True) if restart_nm: # Flush all IP addresses of NM managed interfaces, to avoid NM creating # new, non netplan-* connection profiles, using the existing IPs. for iface in utils.nm_interfaces(restart_nm_glob, devices): utils.ip_addr_flush(iface) utils.systemctl_network_manager('start', sync=sync)
def command_apply( run_generate=True, sync=False, exit_on_error=True): # pragma: nocover (covered in autopkgtest) if run_generate and subprocess.call([utils.get_generator_path()]) != 0: if exit_on_error: sys.exit(os.EX_CONFIG) else: raise ConfigurationError( "the configuration could not be generated") config_manager = ConfigManager() devices = netifaces.interfaces() restart_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) restart_nm = bool( glob.glob('/run/NetworkManager/system-connections/netplan-*')) # stop backends if restart_networkd: logging.debug( 'netplan generated networkd configuration exists, restarting networkd' ) utils.systemctl_networkd('stop', sync=sync, extra_services=['netplan-wpa@*.service']) else: logging.debug('no netplan generated networkd configuration exists') if restart_nm: logging.debug( 'netplan generated NM configuration exists, restarting NM') if utils.nm_running(): # restarting NM does not cause new config to be applied, need to shut down devices first for device in devices: # ignore failures here -- some/many devices might not be managed by NM try: utils.nmcli(['device', 'disconnect', device]) except subprocess.CalledProcessError: pass utils.systemctl_network_manager('stop', sync=sync) else: logging.debug('no netplan generated NM configuration exists') # evaluate config for extra steps we need to take (like renaming) # for now, only applies to non-virtual (real) devices. config_manager.parse() changes = NetplanApply.process_link_changes(devices, config_manager) # if the interface is up, we can still apply some .link file changes for device in devices: logging.debug('netplan triggering .link rules for %s', device) subprocess.check_call([ 'udevadm', 'test-builtin', 'net_setup_link', '/sys/class/net/' + device ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) # apply renames to "down" devices for iface, settings in changes.items(): if settings.get('name'): subprocess.check_call([ 'ip', 'link', 'set', 'dev', iface, 'name', settings.get('name') ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) subprocess.check_call(['udevadm', 'settle']) # (re)start backends if restart_networkd: netplan_wpa = [ os.path.basename(f) for f in glob.glob( '/run/systemd/system/*.wants/netplan-wpa@*.service') ] utils.systemctl_networkd('start', sync=sync, extra_services=netplan_wpa) if restart_nm: utils.systemctl_network_manager('start', sync=sync)
class TestConfigManager(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() self.configmanager = ConfigManager(prefix=self.workdir.name, extra_files={}) os.makedirs(os.path.join(self.workdir.name, "etc/netplan")) os.makedirs(os.path.join(self.workdir.name, "run/systemd/network")) os.makedirs( os.path.join(self.workdir.name, "run/NetworkManager/system-connections")) with open(os.path.join(self.workdir.name, "newfile.yaml"), 'w') as fd: print('''network: version: 2 ethernets: ethtest: dhcp4: yes ''', file=fd) with open(os.path.join(self.workdir.name, "newfile_merging.yaml"), 'w') as fd: print('''network: version: 2 ethernets: eth0: dhcp6: on ethbr1: dhcp4: on ''', file=fd) with open(os.path.join(self.workdir.name, "newfile_emptydict.yaml"), 'w') as fd: print('''network: version: 2 ethernets: eth0: {} bridges: br666: {} ''', file=fd) with open(os.path.join(self.workdir.name, "ovs_merging.yaml"), 'w') as fd: print('''network: version: 2 openvswitch: ports: [[patchx, patcha], [patchy, patchb]] bridges: ovs0: {openvswitch: {}} ''', file=fd) with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd openvswitch: ports: [[patcha, patchb]] other-config: disable-in-band: true ethernets: eth0: dhcp4: false ethbr1: dhcp4: false ethbr2: dhcp4: false ethbond1: dhcp4: false ethbond2: dhcp4: false wifis: wlan1: access-points: testAP: {} modems: wwan0: apn: internet pin: 1234 dhcp4: yes addresses: [1.2.3.4/24, 5.6.7.8/24] vlans: vlan2: id: 2 link: eth99 bridges: br3: interfaces: [ ethbr1 ] br4: interfaces: [ ethbr2 ] parameters: stp: on bonds: bond5: interfaces: [ ethbond1 ] bond6: interfaces: [ ethbond2 ] parameters: mode: 802.3ad tunnels: he-ipv6: mode: sit remote: 2.2.2.2 local: 1.1.1.1 addresses: - "2001:dead:beef::2/64" gateway6: "2001:dead:beef::1" ''', file=fd) with open( os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'w') as fd: print("pretend .network", file=fd) with open( os.path.join(self.workdir.name, "run/NetworkManager/system-connections/pretend"), 'w') as fd: print("pretend NM config", file=fd) def test_parse(self): self.configmanager.parse() self.assertIn('eth0', self.configmanager.ethernets) self.assertIn('bond6', self.configmanager.bonds) self.assertIn('eth0', self.configmanager.physical_interfaces) self.assertNotIn('bond7', self.configmanager.interfaces) self.assertNotIn('bond6', self.configmanager.physical_interfaces) self.assertNotIn('parameters', self.configmanager.bonds.get('bond5')) self.assertIn('parameters', self.configmanager.bonds.get('bond6')) self.assertIn('wwan0', self.configmanager.modems) self.assertIn('wwan0', self.configmanager.physical_interfaces) self.assertIn('apn', self.configmanager.modems.get('wwan0')) self.assertIn('he-ipv6', self.configmanager.tunnels) self.assertNotIn('he-ipv6', self.configmanager.physical_interfaces) self.assertIn('remote', self.configmanager.tunnels.get('he-ipv6')) self.assertIn('other-config', self.configmanager.openvswitch) self.assertIn('ports', self.configmanager.openvswitch) self.assertEquals(2, self.configmanager.version) self.assertEquals('networkd', self.configmanager.renderer) def test_parse_merging(self): self.configmanager.parse(extra_config=[ os.path.join(self.workdir.name, "newfile_merging.yaml") ]) self.assertIn('eth0', self.configmanager.ethernets) self.assertIn('dhcp4', self.configmanager.ethernets['eth0']) self.assertEquals(True, self.configmanager.ethernets['eth0'].get('dhcp6')) self.assertEquals(True, self.configmanager.ethernets['ethbr1'].get('dhcp4')) def test_parse_merging_ovs(self): self.configmanager.parse( extra_config=[os.path.join(self.workdir.name, "ovs_merging.yaml")]) self.assertIn('eth0', self.configmanager.ethernets) self.assertIn('dhcp4', self.configmanager.ethernets['eth0']) self.assertIn('patchx', self.configmanager.ovs_ports) self.assertIn('patchy', self.configmanager.ovs_ports) self.assertIn('ovs0', self.configmanager.bridges) self.assertEqual( {}, self.configmanager.ovs_ports['patchx'].get('openvswitch')) self.assertEqual( {}, self.configmanager.ovs_ports['patchy'].get('openvswitch')) self.assertEqual({}, self.configmanager.bridges['ovs0'].get('openvswitch')) def test_parse_emptydict(self): self.configmanager.parse(extra_config=[ os.path.join(self.workdir.name, "newfile_emptydict.yaml") ]) self.assertIn('br666', self.configmanager.bridges) self.assertEquals(False, self.configmanager.ethernets['eth0'].get('dhcp4')) self.assertEquals(False, self.configmanager.ethernets['ethbr1'].get('dhcp4')) def test_parse_extra_config(self): self.configmanager.parse( extra_config=[os.path.join(self.workdir.name, "newfile.yaml")]) self.assertIn('ethtest', self.configmanager.ethernets) self.assertIn('bond6', self.configmanager.bonds) def test_add(self): self.configmanager.add({ os.path.join(self.workdir.name, "newfile.yaml"): os.path.join(self.workdir.name, "etc/netplan/newfile.yaml") }) self.assertIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertTrue( os.path.exists( os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) def test_backup_missing_dirs(self): backup_dir = self.configmanager.tempdir shutil.rmtree(os.path.join(self.workdir.name, "run/systemd/network")) self.configmanager.backup(backup_config_dir=False) self.assertTrue( os.path.exists( os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) # no source dir means no backup as well self.assertFalse( os.path.exists( os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertFalse( os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_backup_without_config_file(self): backup_dir = self.configmanager.tempdir self.configmanager.backup(backup_config_dir=False) self.assertTrue( os.path.exists( os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) self.assertTrue( os.path.exists( os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertFalse( os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_backup_with_config_file(self): backup_dir = self.configmanager.tempdir self.configmanager.backup(backup_config_dir=True) self.assertTrue( os.path.exists( os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) self.assertTrue( os.path.exists( os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertTrue( os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_revert(self): self.configmanager.backup() with open( os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'a+') as fd: print("CHANGED", file=fd) with open( os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'r') as fd: lines = fd.readlines() self.assertIn("CHANGED\n", lines) self.configmanager.revert() with open( os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'r') as fd: lines = fd.readlines() self.assertNotIn("CHANGED\n", lines) def test_revert_extra_files(self): self.configmanager.add({ os.path.join(self.workdir.name, "newfile.yaml"): os.path.join(self.workdir.name, "etc/netplan/newfile.yaml") }) self.assertIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertTrue( os.path.exists( os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) self.configmanager.revert() self.assertNotIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertFalse( os.path.exists( os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) def test_cleanup(self): backup_dir = self.configmanager.tempdir self.assertTrue(os.path.exists(backup_dir)) self.configmanager.cleanup() self.assertFalse(os.path.exists(backup_dir)) def test__copy_tree(self): self.configmanager._copy_tree(os.path.join(self.workdir.name, "etc"), os.path.join(self.workdir.name, "etc2")) self.assertTrue( os.path.exists( os.path.join(self.workdir.name, "etc2/netplan/test.yaml"))) def test__copy_tree_missing_source(self): with self.assertRaises(FileNotFoundError): self.configmanager._copy_tree(os.path.join(self.workdir.name, "nonexistent"), os.path.join(self.workdir.name, "nonexistent2"), missing_ok=False)
class TestSRIOV(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() os.makedirs(os.path.join(self.workdir.name, 'etc/netplan')) self.configmanager = ConfigManager(prefix=self.workdir.name, extra_files={}) def _prepare_sysfs_dir_structure(self): # prepare a directory hierarchy for testing the matching # this might look really scary, but that's how sysfs presents devices # such as these os.makedirs(os.path.join(self.workdir.name, 'sys/class/net')) # first the VF vf_iface_path = os.path.join( self.workdir.name, 'sys/devices/pci0000:00/0000:00:1f.6/net/enp2s16f1') vf_dev_path = os.path.join(self.workdir.name, 'sys/devices/pci0000:00/0000:00:1f.6') os.makedirs(vf_iface_path) with open(os.path.join(vf_dev_path, 'vendor'), 'w') as f: f.write('0x001f\n') with open(os.path.join(vf_dev_path, 'device'), 'w') as f: f.write('0xb33f\n') os.symlink('../../devices/pci0000:00/0000:00:1f.6/net/enp2s16f1', os.path.join(self.workdir.name, 'sys/class/net/enp2s16f1')) os.symlink( '../../../0000:00:1f.6', os.path.join(self.workdir.name, 'sys/class/net/enp2s16f1/device')) # now the PF os.path.join(self.workdir.name, 'sys/class/net/enp2') pf_iface_path = os.path.join( self.workdir.name, 'sys/devices/pci0000:00/0000:00:1f.0/net/enp2') pf_dev_path = os.path.join(self.workdir.name, 'sys/devices/pci0000:00/0000:00:1f.0') os.makedirs(pf_iface_path) with open(os.path.join(pf_dev_path, 'vendor'), 'w') as f: f.write('0x001f\n') with open(os.path.join(pf_dev_path, 'device'), 'w') as f: f.write('0x1337\n') os.symlink('../../devices/pci0000:00/0000:00:1f.0/net/enp2', os.path.join(self.workdir.name, 'sys/class/net/enp2')) os.symlink( '../../../0000:00:1f.0', os.path.join(self.workdir.name, 'sys/class/net/enp2/device')) # the PF additionally has device links to all the VFs defined for it os.symlink('../../../0000:00:1f.4', os.path.join(pf_dev_path, 'virtfn1')) os.symlink('../../../0000:00:1f.5', os.path.join(pf_dev_path, 'virtfn2')) os.symlink('../../../0000:00:1f.6', os.path.join(pf_dev_path, 'virtfn3')) os.symlink('../../../0000:00:1f.7', os.path.join(pf_dev_path, 'virtfn4')) @patch('netplan.cli.utils.get_interface_driver_name') @patch('netplan.cli.utils.get_interface_macaddress') def test_get_vf_count_and_functions(self, gim, gidn): # we mock-out get_interface_driver_name and get_interface_macaddress # to return useful values for the test gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00' gidn.side_effect = lambda x: 'foo' if x == 'enp2' else 'bar' with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: renderer: networkd enp1: mtu: 9000 enp2: match: driver: foo enp3: match: macaddress: 00:01:02:03:04:05 enpx: match: name: enp[4-5] enp0: mtu: 9000 enp8: virtual-function-count: 7 enp9: {} wlp6s0: {} enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 macaddress: 01:02:03:04:05:01 enp2s16f1: link: enp2 enp2s16f2: {link: enp2} enp3s16f1: link: enp3 enpxs16f1: match: name: enp[4-5]s16f1 link: enpx enp9s16f1: link: enp9 ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'enp2', 'enp3', 'enp5', 'enp0', 'enp8'] vf_counts = defaultdict(int) vfs = {} pfs = {} # call the function under test sriov.get_vf_count_and_functions(interfaces, self.configmanager, vf_counts, vfs, pfs) # check if the right vf counts have been recorded in vf_counts self.assertDictEqual(vf_counts, { 'enp1': 2, 'enp2': 2, 'enp3': 1, 'enp5': 1, 'enp8': 7 }) # also check if the vfs and pfs dictionaries got properly set self.assertDictEqual( vfs, { 'enp1s16f1': None, 'enp1s16f2': None, 'enp2s16f1': None, 'enp2s16f2': None, 'enp3s16f1': None, 'enpxs16f1': None }) self.assertDictEqual( pfs, { 'enp1': 'enp1', 'enp2': 'enp2', 'enp3': 'enp3', 'enpx': 'enp5', 'enp8': 'enp8' }) @patch('netplan.cli.utils.get_interface_driver_name') @patch('netplan.cli.utils.get_interface_macaddress') def test_get_vf_count_and_functions_many_match(self, gim, gidn): # we mock-out get_interface_driver_name and get_interface_macaddress # to return useful values for the test gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00' gidn.side_effect = lambda x: 'foo' if x == 'enp2' else 'bar' with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: renderer: networkd enpx: match: name: enp* mtu: 9000 enpxs16f1: link: enpx ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'wlp6s0', 'enp2', 'enp3'] vf_counts = defaultdict(int) vfs = {} pfs = {} # call the function under test with self.assertRaises(ConfigurationError) as e: sriov.get_vf_count_and_functions(interfaces, self.configmanager, vf_counts, vfs, pfs) self.assertIn('matched more than one interface for a PF device: enpx', str(e.exception)) @patch('netplan.cli.utils.get_interface_driver_name') @patch('netplan.cli.utils.get_interface_macaddress') def test_get_vf_count_and_functions_not_enough_explicit(self, gim, gidn): # we mock-out get_interface_driver_name and get_interface_macaddress # to return useful values for the test gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'enp3' else '00:00:00:00:00:00' gidn.side_effect = lambda x: 'foo' if x == 'enp2' else 'bar' with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: renderer: networkd enp1: virtual-function-count: 2 mtu: 9000 enp1s16f1: link: enp1 enp1s16f2: link: enp1 enp1s16f3: link: enp1 ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'wlp6s0'] vf_counts = defaultdict(int) vfs = {} pfs = {} # call the function under test with self.assertRaises(ConfigurationError) as e: sriov.get_vf_count_and_functions(interfaces, self.configmanager, vf_counts, vfs, pfs) self.assertIn( 'more VFs allocated than the explicit size declared: 3 > 2', str(e.exception)) def test_set_numvfs_for_pf(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['8\n'] with patch('builtins.open', sriov_open.open): ret = sriov.set_numvfs_for_pf('enp1', 2) self.assertTrue(ret) self.assertListEqual(sriov_open.open.call_args_list, [ call('/sys/class/net/enp1/device/sriov_totalvfs'), call('/sys/class/net/enp1/device/sriov_numvfs', 'w') ]) handle = sriov_open.open() handle.write.assert_called_once_with('2') def test_set_numvfs_for_pf_failsafe(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['8\n'] sriov_open.write_queue = [IOError(16, 'Error'), None, None] with patch('builtins.open', sriov_open.open): ret = sriov.set_numvfs_for_pf('enp1', 2) self.assertTrue(ret) handle = sriov_open.open() self.assertEqual(handle.write.call_count, 3) def test_set_numvfs_for_pf_over_max(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['8\n'] with patch('builtins.open', sriov_open.open): with self.assertRaises(ConfigurationError) as e: sriov.set_numvfs_for_pf('enp1', 9) self.assertIn( 'cannot allocate more VFs for PF enp1 than supported', str(e.exception)) def test_set_numvfs_for_pf_over_theoretical_max(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['1337\n'] with patch('builtins.open', sriov_open.open): with self.assertRaises(ConfigurationError) as e: sriov.set_numvfs_for_pf('enp1', 345) self.assertIn( 'cannot allocate more VFs for PF enp1 than the SR-IOV maximum', str(e.exception)) def test_set_numvfs_for_pf_read_failed(self): sriov_open = MockSRIOVOpen() cases = ( [IOError], ['not a number\n'], ) with patch('builtins.open', sriov_open.open): for case in cases: sriov_open.read_queue = case with self.assertRaises(RuntimeError): sriov.set_numvfs_for_pf('enp1', 3) def test_set_numvfs_for_pf_write_failed(self): sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['8\n'] sriov_open.write_queue = [IOError(16, 'Error'), IOError(16, 'Error')] with patch('builtins.open', sriov_open.open): with self.assertRaises(RuntimeError) as e: sriov.set_numvfs_for_pf('enp1', 2) self.assertIn('failed setting sriov_numvfs to 2 for enp1', str(e.exception)) def test_perform_hardware_specific_quirks(self): # for now we have no custom quirks defined, so we just # check if the function succeeds sriov_open = MockSRIOVOpen() sriov_open.read_queue = ['0x001f\n', '0x1337\n'] with patch('builtins.open', sriov_open.open): sriov.perform_hardware_specific_quirks('enp1') # it's good enough if it did all the matching self.assertListEqual(sriov_open.open.call_args_list, [ call('/sys/class/net/enp1/device/vendor'), call('/sys/class/net/enp1/device/device'), ]) def test_perform_hardware_specific_quirks_failed(self): sriov_open = MockSRIOVOpen() cases = ( [IOError], ['0x001f\n', IOError], ) with patch('builtins.open', sriov_open.open): for case in cases: sriov_open.read_queue = case with self.assertRaises(RuntimeError) as e: sriov.perform_hardware_specific_quirks('enp1') self.assertIn( 'could not determine vendor and device ID of enp1', str(e.exception)) @patch('subprocess.check_call') def test_apply_vlan_filter_for_vf(self, check_call): self._prepare_sysfs_dir_structure() sriov.apply_vlan_filter_for_vf('enp2', 'enp2s16f1', 'vlan10', 10, prefix=self.workdir.name) self.assertEqual(check_call.call_count, 1) self.assertListEqual( check_call.call_args[0][0], ['ip', 'link', 'set', 'dev', 'enp2', 'vf', '3', 'vlan', '10']) @patch('subprocess.check_call') def test_apply_vlan_filter_for_vf_failed_no_index(self, check_call): self._prepare_sysfs_dir_structure() # we remove the PF -> VF link, simulating a system error os.unlink( os.path.join(self.workdir.name, 'sys/class/net/enp2/device/virtfn3')) with self.assertRaises(RuntimeError) as e: sriov.apply_vlan_filter_for_vf('enp2', 'enp2s16f1', 'vlan10', 10, prefix=self.workdir.name) self.assertIn( 'could not determine the VF index for enp2s16f1 while configuring vlan vlan10', str(e.exception)) self.assertEqual(check_call.call_count, 0) @patch('subprocess.check_call') def test_apply_vlan_filter_for_vf_failed_ip_link_set(self, check_call): self._prepare_sysfs_dir_structure() check_call.side_effect = CalledProcessError(-1, None) with self.assertRaises(RuntimeError) as e: sriov.apply_vlan_filter_for_vf('enp2', 'enp2s16f1', 'vlan10', 10, prefix=self.workdir.name) self.assertIn('failed setting SR-IOV VLAN filter for vlan vlan10', str(e.exception)) @patch('netifaces.interfaces') @patch('netplan.cli.sriov.get_vf_count_and_functions') @patch('netplan.cli.sriov.set_numvfs_for_pf') @patch('netplan.cli.sriov.perform_hardware_specific_quirks') @patch('netplan.cli.sriov.apply_vlan_filter_for_vf') @patch('netplan.cli.utils.get_interface_driver_name') @patch('netplan.cli.utils.get_interface_macaddress') def test_apply_sriov_config(self, gim, gidn, apply_vlan, quirks, set_numvfs, get_counts, netifs): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: mtu: 9000 enpx: match: name: enp[2-3] enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 customvf1: match: name: enp[2-3]s16f[1-4] link: enpx vlans: vf1.15: renderer: sriov id: 15 link: customvf1 vf1.16: renderer: sriov id: 16 link: foobar ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'enp2', 'enp5', 'wlp6s0'] # set up all the mock objects netifs.return_value = [ 'enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1' ] get_counts.side_effect = mock_set_counts set_numvfs.side_effect = lambda pf, _: False if pf == 'enp2' else True gidn.return_value = 'foodriver' gim.return_value = '00:01:02:03:04:05' # call method under test sriov.apply_sriov_config(interfaces, self.configmanager) # check if the config got applied as expected # we had 2 PFs, one having two VFs and the other only one self.assertEqual(set_numvfs.call_count, 2) self.assertListEqual( set_numvfs.call_args_list, [call('enp1', 2), call('enp2', 1)]) # one of the pfs already had sufficient VFs allocated, so only enp1 # changed the vf count and only that one should trigger quirks quirks.assert_called_once_with('enp1') # only one had a hardware vlan apply_vlan.assert_called_once_with('enp2', 'enp2s16f1', 'vf1.15', 15) @patch('netifaces.interfaces') @patch('netplan.cli.sriov.get_vf_count_and_functions') @patch('netplan.cli.sriov.set_numvfs_for_pf') @patch('netplan.cli.sriov.perform_hardware_specific_quirks') @patch('netplan.cli.sriov.apply_vlan_filter_for_vf') @patch('netplan.cli.utils.get_interface_driver_name') @patch('netplan.cli.utils.get_interface_macaddress') def test_apply_sriov_config_invalid_vlan(self, gim, gidn, apply_vlan, quirks, set_numvfs, get_counts, netifs): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: mtu: 9000 enpx: match: name: enp[2-3] enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 customvf1: match: name: enp[2-3]s16f[1-4] link: enpx vlans: vf1.15: renderer: sriov link: customvf1 ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'enp2', 'enp5', 'wlp6s0'] # set up all the mock objects netifs.return_value = [ 'enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1' ] get_counts.side_effect = mock_set_counts set_numvfs.side_effect = lambda pf, _: False if pf == 'enp2' else True gidn.return_value = 'foodriver' gim.return_value = '00:01:02:03:04:05' # call method under test with self.assertRaises(ConfigurationError) as e: sriov.apply_sriov_config(interfaces, self.configmanager) self.assertIn('no id property defined for SR-IOV vlan vf1.15', str(e.exception)) self.assertEqual(apply_vlan.call_count, 0) @patch('netifaces.interfaces') @patch('netplan.cli.sriov.get_vf_count_and_functions') @patch('netplan.cli.sriov.set_numvfs_for_pf') @patch('netplan.cli.sriov.perform_hardware_specific_quirks') @patch('netplan.cli.sriov.apply_vlan_filter_for_vf') @patch('netplan.cli.utils.get_interface_driver_name') @patch('netplan.cli.utils.get_interface_macaddress') def test_apply_sriov_config_too_many_vlans(self, gim, gidn, apply_vlan, quirks, set_numvfs, get_counts, netifs): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: mtu: 9000 enpx: match: name: enp[2-3] enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 customvf1: match: name: enp[2-3]s16f[1-4] link: enpx vlans: vf1.15: renderer: sriov id: 15 link: customvf1 vf1.16: renderer: sriov id: 16 link: customvf1 ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'enp2', 'enp5', 'wlp6s0'] # set up all the mock objects netifs.return_value = [ 'enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1' ] get_counts.side_effect = mock_set_counts set_numvfs.side_effect = lambda pf, _: False if pf == 'enp2' else True gidn.return_value = 'foodriver' gim.return_value = '00:01:02:03:04:05' # call method under test with self.assertRaises(ConfigurationError) as e: sriov.apply_sriov_config(interfaces, self.configmanager) self.assertIn( 'interface enp2s16f1 for netplan device customvf1 (vf1.16) already has an SR-IOV vlan defined', str(e.exception)) self.assertEqual(apply_vlan.call_count, 1) @patch('netifaces.interfaces') @patch('netplan.cli.sriov.get_vf_count_and_functions') @patch('netplan.cli.sriov.set_numvfs_for_pf') @patch('netplan.cli.sriov.perform_hardware_specific_quirks') @patch('netplan.cli.sriov.apply_vlan_filter_for_vf') @patch('netplan.cli.utils.get_interface_driver_name') @patch('netplan.cli.utils.get_interface_macaddress') def test_apply_sriov_config_many_match(self, gim, gidn, apply_vlan, quirks, set_numvfs, get_counts, netifs): # set up the environment with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: enp1: mtu: 9000 enpx: match: name: enp[2-3] enp1s16f1: link: enp1 macaddress: 01:02:03:04:05:00 enp1s16f2: link: enp1 customvf1: match: name: enp*s16f[1-4] link: enpx ''', file=fd) self.configmanager.parse() interfaces = ['enp1', 'enp2', 'enp5', 'wlp6s0'] # set up all the mock objects netifs.return_value = [ 'enp1', 'enp2', 'enp5', 'wlp6s0', 'enp1s16f1', 'enp1s16f2', 'enp2s16f1' ] get_counts.side_effect = mock_set_counts set_numvfs.side_effect = lambda pf, _: False if pf == 'enp2' else True gidn.return_value = 'foodriver' gim.return_value = '00:01:02:03:04:05' # call method under test with self.assertRaises(ConfigurationError) as e: sriov.apply_sriov_config(interfaces, self.configmanager) self.assertIn( 'matched more than one interface for a VF device: customvf1', str(e.exception))
class TestConfigManager(unittest.TestCase): def setUp(self): self.workdir = tempfile.TemporaryDirectory() self.configmanager = ConfigManager(prefix=self.workdir.name, extra_files={}) os.makedirs(os.path.join(self.workdir.name, "etc/netplan")) os.makedirs(os.path.join(self.workdir.name, "run/systemd/network")) os.makedirs(os.path.join(self.workdir.name, "run/NetworkManager/system-connections")) with open(os.path.join(self.workdir.name, "newfile.yaml"), 'w') as fd: print('''network: version: 2 ethernets: ethtest: dhcp4: yes ''', file=fd) with open(os.path.join(self.workdir.name, "newfile_merging.yaml"), 'w') as fd: print('''network: version: 2 ethernets: eth0: dhcp6: on ethbr1: dhcp4: on ''', file=fd) with open(os.path.join(self.workdir.name, "newfile_emptydict.yaml"), 'w') as fd: print('''network: version: 2 ethernets: eth0: {} bridges: br666: {} ''', file=fd) with open(os.path.join(self.workdir.name, "etc/netplan/test.yaml"), 'w') as fd: print('''network: version: 2 renderer: networkd ethernets: eth0: dhcp4: false ethbr1: dhcp4: false ethbr2: dhcp4: false ethbond1: dhcp4: false ethbond2: dhcp4: false wifis: wlan1: access-points: testAP: {} vlans: vlan2: id: 2 link: eth99 bridges: br3: interfaces: [ ethbr1 ] br4: interfaces: [ ethbr2 ] parameters: stp: on bonds: bond5: interfaces: [ ethbond1 ] bond6: interfaces: [ ethbond2 ] parameters: mode: 802.3ad ''', file=fd) with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'w') as fd: print("pretend .network", file=fd) with open(os.path.join(self.workdir.name, "run/NetworkManager/system-connections/pretend"), 'w') as fd: print("pretend NM config", file=fd) def test_parse(self): self.configmanager.parse() self.assertIn('eth0', self.configmanager.ethernets) self.assertIn('bond6', self.configmanager.bonds) self.assertIn('eth0', self.configmanager.physical_interfaces) self.assertNotIn('bond7', self.configmanager.interfaces) self.assertNotIn('bond6', self.configmanager.physical_interfaces) self.assertNotIn('parameters', self.configmanager.bonds.get('bond5')) self.assertIn('parameters', self.configmanager.bonds.get('bond6')) def test_parse_merging(self): self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile_merging.yaml")]) self.assertIn('eth0', self.configmanager.ethernets) self.assertIn('dhcp4', self.configmanager.ethernets['eth0']) self.assertEquals(True, self.configmanager.ethernets['eth0'].get('dhcp6')) self.assertEquals(True, self.configmanager.ethernets['ethbr1'].get('dhcp4')) def test_parse_emptydict(self): self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile_emptydict.yaml")]) self.assertIn('br666', self.configmanager.bridges) self.assertEquals(False, self.configmanager.ethernets['eth0'].get('dhcp4')) self.assertEquals(False, self.configmanager.ethernets['ethbr1'].get('dhcp4')) def test_parse_extra_config(self): self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile.yaml")]) self.assertIn('ethtest', self.configmanager.ethernets) self.assertIn('bond6', self.configmanager.bonds) def test_add(self): self.configmanager.add({os.path.join(self.workdir.name, "newfile.yaml"): os.path.join(self.workdir.name, "etc/netplan/newfile.yaml")}) self.assertIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertTrue(os.path.exists(os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) def test_backup_missing_dirs(self): backup_dir = self.configmanager.tempdir shutil.rmtree(os.path.join(self.workdir.name, "run/systemd/network")) self.configmanager.backup(backup_config_dir=False) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) # no source dir means no backup as well self.assertFalse(os.path.exists(os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertFalse(os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_backup_without_config_file(self): backup_dir = self.configmanager.tempdir self.configmanager.backup(backup_config_dir=False) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertFalse(os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_backup_with_config_file(self): backup_dir = self.configmanager.tempdir self.configmanager.backup(backup_config_dir=True) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/NetworkManager/system-connections/pretend"))) self.assertTrue(os.path.exists(os.path.join(backup_dir, "run/systemd/network/01-pretend.network"))) self.assertTrue(os.path.exists(os.path.join(backup_dir, "etc/netplan/test.yaml"))) def test_revert(self): self.configmanager.backup() with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'a+') as fd: print("CHANGED", file=fd) with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'r') as fd: lines = fd.readlines() self.assertIn("CHANGED\n", lines) self.configmanager.revert() with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'r') as fd: lines = fd.readlines() self.assertNotIn("CHANGED\n", lines) def test_revert_extra_files(self): self.configmanager.add({os.path.join(self.workdir.name, "newfile.yaml"): os.path.join(self.workdir.name, "etc/netplan/newfile.yaml")}) self.assertIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertTrue(os.path.exists(os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) self.configmanager.revert() self.assertNotIn(os.path.join(self.workdir.name, "newfile.yaml"), self.configmanager.extra_files) self.assertFalse(os.path.exists(os.path.join(self.workdir.name, "etc/netplan/newfile.yaml"))) def test_cleanup(self): backup_dir = self.configmanager.tempdir self.assertTrue(os.path.exists(backup_dir)) self.configmanager.cleanup() self.assertFalse(os.path.exists(backup_dir)) def test__copy_tree(self): self.configmanager._copy_tree(os.path.join(self.workdir.name, "etc"), os.path.join(self.workdir.name, "etc2")) self.assertTrue(os.path.exists(os.path.join(self.workdir.name, "etc2/netplan/test.yaml"))) @unittest.expectedFailure def test__copy_tree_missing_source(self): self.configmanager._copy_tree(os.path.join(self.workdir.name, "nonexistent"), os.path.join(self.workdir.name, "nonexistent2"), missing_ok=False)