Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
 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()
Ejemplo n.º 3
0
    def write_file(self, set_tree, name, rootdir='/'):
        tmproot = tempfile.TemporaryDirectory(prefix='netplan-set_')
        path = os.path.join('etc', 'netplan')
        os.makedirs(os.path.join(tmproot.name, path))

        config = {'network': {}}
        absp = os.path.join(rootdir, path, name)
        if os.path.isfile(absp):
            with open(absp, 'r') as f:
                config = yaml.safe_load(f)

        new_tree = self.merge(config, set_tree)
        stripped = ConfigManager.strip_tree(new_tree)
        if 'network' in stripped:
            tmpp = os.path.join(tmproot.name, path, name)
            with open(tmpp, 'w+') as f:
                new_yaml = yaml.dump(stripped,
                                     indent=2,
                                     default_flow_style=False)
                f.write(new_yaml)
            # Validate the newly created file, by parsing it via libnetplan
            utils.netplan_parse(tmpp)
            # Valid, move it to final destination
            os.replace(tmpp, absp)
        elif os.path.isfile(absp):
            # Clear file if the last/only key got removed
            os.remove(absp)
        else:
            raise Exception('Invalid input: {}'.format(set_tree))
Ejemplo n.º 4
0
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()
Ejemplo n.º 5
0
    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)
Ejemplo n.º 6
0
    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)
Ejemplo n.º 7
0
 def config_manager(self):  # pragma: nocover (called by later commands)
     if not self._config_manager:
         self._config_manager = ConfigManager()
     return self._config_manager
Ejemplo n.º 8
0
    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)
Ejemplo n.º 9
0
    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)
Ejemplo n.º 10
0
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)
 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={})
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))
    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)
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)