From 5753510f284571b5fad75844a420d6fcdec3741c Mon Sep 17 00:00:00 2001 From: Mathieu Trudel-Lapierre Date: Tue, 27 Aug 2019 13:11:27 -0400 Subject: [PATCH 1/3] Refresh devices after restarting backends, some new devices might appear Gbp-Pq: Name 0001-Refresh-devices-after-restarting-backends-some-new-d.patch --- netplan/cli/commands/apply.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index 3e7019d..30a5013 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -118,6 +118,9 @@ class NetplanApply(utils.NetplanCommand): 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() -- cgit v1.2.3 From e4e25730afcee44035d1bd1eba03ff81ac6a8f20 Mon Sep 17 00:00:00 2001 From: Kexy Biscuit Date: Thu, 3 Oct 2019 04:37:18 +0800 Subject: [PATCH 2/3] Makefile: fix escaping in _features.h (#103) Gbp-Pq: Name 0002-Makefile-fix-escaping-in-_features.h-103.patch --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d89eb26..c9921ed 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ netplan-dbus: src/dbus.c src/_features.h $(CC) $(BUILDFLAGS) $(CFLAGS) -o $@ $^ `pkg-config --cflags --libs libsystemd glib-2.0` src/_features.h: src/[^_]*.[hc] - echo "#include \nstatic const char *feature_flags[] __attribute__((__unused__)) = {" > $@ + printf "#include \nstatic const char *feature_flags[] __attribute__((__unused__)) = {\n" > $@ awk 'match ($$0, /netplan-feature:.*/ ) { $$0=substr($$0, RSTART, RLENGTH); print "\""$$2"\"," }' $^ >> $@ echo "NULL, };" >> $@ -- cgit v1.2.3 From bc43030de551a8a908eaa29ff87f8f77333931e5 Mon Sep 17 00:00:00 2001 From: Conrad Hoffmann <1226676+bitfehler@users.noreply.github.com> Date: Fri, 4 Oct 2019 00:21:22 +0200 Subject: [PATCH 3/3] Honor LDFLAGS when building netplan-dbus (#105) This is a follow-up to #91, not sure how I missed this, sorry about that. Gbp-Pq: Name 0003-Honor-LDFLAGS-when-building-netplan-dbus-105.patch --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c9921ed..e3db52d 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ generate: src/generate.[hc] src/parse.[hc] src/util.[hc] src/networkd.[hc] src/n $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $(filter %.c, $^) `pkg-config --cflags --libs glib-2.0 gio-2.0 yaml-0.1 uuid` netplan-dbus: src/dbus.c src/_features.h - $(CC) $(BUILDFLAGS) $(CFLAGS) -o $@ $^ `pkg-config --cflags --libs libsystemd glib-2.0` + $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ `pkg-config --cflags --libs libsystemd glib-2.0` src/_features.h: src/[^_]*.[hc] printf "#include \nstatic const char *feature_flags[] __attribute__((__unused__)) = {\n" > $@ -- cgit v1.2.3 From 6e30eab9b57578e9da4c96eb873e1ac1d7c69823 Mon Sep 17 00:00:00 2001 From: Lukas Maerdian Date: Tue, 28 Apr 2020 14:35:36 +0200 Subject: Fix LP#1874377: Not connect to WiFi after 'netplan apply' (#133) * Fix LP#1874377: Not connect to WiFi after 'netplan apply' Seems like the 'netplan apply' command was not properly adopted in #109 when wired wpa_supplicant support was introduced. Fixes: https://bugs.launchpad.net/ubuntu/+source/netplan.io/+bug/1874377 Gbp-Pq: Name 0001-Fix-LP-1874377-Not-connect-to-WiFi-after-netplan-app.patch --- netplan/cli/commands/apply.py | 10 ++++-- netplan/cli/utils.py | 7 +++++ src/networkd.c | 4 +++ tests/generator/test_wifis.py | 71 +++++++++++++++++++++++++++++++++++++++++++ tests/integration/base.py | 2 +- 5 files changed, 91 insertions(+), 3 deletions(-) diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index 0ec95f5..cf9f122 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -108,7 +108,13 @@ class NetplanApply(utils.NetplanCommand): # 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']) + 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_networkd('stop', sync=sync, extra_services=wpa_services) + else: logging.debug('no netplan generated networkd configuration exists') @@ -169,7 +175,7 @@ class NetplanApply(utils.NetplanCommand): # (re)start backends if restart_networkd: - netplan_wpa = [os.path.basename(f) for f in glob.glob('/run/systemd/system/*.wants/netplan-wpa@*.service')] + 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) diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py index 5f54b1a..c0eee03 100644 --- a/netplan/cli/utils.py +++ b/netplan/cli/utils.py @@ -86,6 +86,13 @@ def systemctl_networkd(action, sync=False, extra_services=[]): # pragma: nocove subprocess.check_call(command) +def systemctl_is_active(unit_pattern): # pragma: nocover (covered in autopkgtest) + '''Return True if at least one matching unit is running''' + if subprocess.call(['systemctl', '--quiet', 'is-active', unit_pattern]) == 0: + return True + return False + + def get_interface_driver_name(interface, only_down=False): # pragma: nocover (covered in autopkgtest) devdir = os.path.join('/sys/class/net', interface) if only_down: diff --git a/src/networkd.c b/src/networkd.c index e2bb111..6f6173a 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -990,8 +990,12 @@ cleanup_networkd_conf(const char* rootdir) { unlink_glob(rootdir, "/run/systemd/network/10-netplan-*"); unlink_glob(rootdir, "/run/netplan/wpa-*.conf"); + unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-*.service"); unlink_glob(rootdir, "/run/systemd/system/netplan-wpa-*.service"); unlink_glob(rootdir, "/run/udev/rules.d/99-netplan-*"); + /* Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an + * upgraded system, we need to make sure to clean those up. */ + unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa@*.service"); } /** diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py index 8eb804e..d5b79cf 100644 --- a/tests/generator/test_wifis.py +++ b/tests/generator/test_wifis.py @@ -116,6 +116,77 @@ network={ self.assertTrue(os.path.islink(os.path.join( self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + def test_wifi_upgrade(self): + # pretend an old 'netplan-wpa@*.service' link still exists on an upgraded system + os.makedirs(os.path.join(self.workdir.name, 'lib/systemd/system')) + os.makedirs(os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants')) + with open(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), 'w') as out: + out.write('''[Unit] +Description=WPA supplicant for netplan %I +DefaultDependencies=no +Requires=sys-subsystem-net-devices-%i.device +After=sys-subsystem-net-devices-%i.device +Before=network.target +Wants=network.target + +[Service] +Type=simple +ExecStart=/sbin/wpa_supplicant -c /run/netplan/wpa-%I.conf -i%I''') + os.symlink(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), + os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service')) + + # run generate, which should cleanup the old files/symlinks + self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "s0s3kr1t" + dhcp4: yes''') + + # verify new files/links exist, while old have been removed + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + # old files/links + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'))) + self.assertFalse(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service'))) + + # pretend another old systemd service file exists for wl1 + os.symlink(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), + os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl1.service')) + + # run generate again, to verify the historical netplan-wpa@.service links and wl0 links are gone + self.generate('''network: + version: 2 + wifis: + wl1: + access-points: + "Other Home": + password: "s0s3kr1t" + dhcp4: yes''') + + # verify new files/links exist, while old have been removed + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl1.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl1.service'))) + # old files/links + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'))) + self.assertFalse(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl1.service'))) + self.assertFalse(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service'))) + self.assertFalse(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) + self.assertFalse(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + def test_wifi_route(self): self.generate('''network: version: 2 diff --git a/tests/integration/base.py b/tests/integration/base.py index ed7100e..16fd2ee 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -93,7 +93,7 @@ class IntegrationTestsBase(unittest.TestCase): pass def tearDown(self): - subprocess.call(['systemctl', 'stop', 'NetworkManager', 'systemd-networkd', 'netplan-wpa@*', + subprocess.call(['systemctl', 'stop', 'NetworkManager', 'systemd-networkd', 'netplan-wpa-*', 'systemd-networkd.socket']) # NM has KillMode=process and leaks dhclient processes subprocess.call(['systemctl', 'kill', 'NetworkManager']) -- cgit v1.2.3 From adf4515eb563bcbb85b9915495adaf56e886490e Mon Sep 17 00:00:00 2001 From: Heitor Alves de Siqueira Date: Thu, 28 May 2020 09:19:07 -0300 Subject: Fix process_link_changes handling 'up' interfaces b7f1d9b04212 refactored process_link_changes with helper methods to get the interface driver name and MAC address. This new code introduced a regression where it's possible for an interface in the up state to be included in the changelist by its MAC address. This patch restores the previous behaviour, skipping interfaces that don't have driver_name set. Fixes: https://bugs.launchpad.net/bugs/1875411 Origin: upstream, https://github.com/CanonicalLtd/netplan/commit/8f77deec17ce Bug-Ubuntu: https://bugs.launchpad.net/bugs/1875411 Gbp-Pq: Name 0002-Fix-process_link_changes-handling-up-interfaces.patch --- netplan/cli/commands/apply.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index cf9f122..4b91ae9 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -244,6 +244,9 @@ class NetplanApply(utils.NetplanCommand): continue driver_name = utils.get_interface_driver_name(interface, only_down=True) + if not driver_name: + # don't allow up interfaces to match by mac + continue macaddress = utils.get_interface_macaddress(interface) if driver_name in matches['by-driver']: new_name = matches['by-driver'][driver_name] -- cgit v1.2.3 From 8d0813f5c0ee9fb23af0c3ceb6202c9211cc0fb4 Mon Sep 17 00:00:00 2001 From: Lukas Maerdian Date: Thu, 14 May 2020 10:12:14 +0200 Subject: [PATCH 1/8] Call daemon-reload after we touched systemd unit files (LP: #1874494) (#135) When running netplan apply systemd unit files might be created/deleted/changed on disk. We need to run systemctl daemon-reload, to make systemd aware of those new units and calculate the new unit/service dependencies. Otherwise it will throw warnings at us: ubuntu@ubuntu:~$ sudo netplan apply Warning: The unit file, source configuration file or drop-ins of netplan-wpa-wlan0.service changed on disk. Run 'systemctl daemon-reload' to reload units. Warning: The unit file, source configuration file or drop-ins of netplan-wpa-wlan0.service changed on disk. Run 'systemctl daemon-reload' to reload units. Fixes: https://bugs.launchpad.net/ubuntu/+source/netplan.io/+bug/1874494 Gbp-Pq: Name 0003-Call-daemon-reload-after-we-touched-systemd-unit-fil.patch --- netplan/cli/commands/apply.py | 4 ++++ netplan/cli/utils.py | 5 +++++ tests/integration/base.py | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index 4b91ae9..33ff5b5 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -108,6 +108,10 @@ class NetplanApply(utils.NetplanCommand): # stop backends if restart_networkd: logging.debug('netplan generated networkd configuration changed, restarting 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() 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. diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py index c0eee03..85342fc 100644 --- a/netplan/cli/utils.py +++ b/netplan/cli/utils.py @@ -93,6 +93,11 @@ def systemctl_is_active(unit_pattern): # pragma: nocover (covered in autopkgtes return False +def systemctl_daemon_reload(): # pragma: nocover (covered in autopkgtest) + '''Reload systemd unit files from disk and re-calculate its dependencies''' + subprocess.check_call(['systemctl', 'daemon-reload']) + + def get_interface_driver_name(interface, only_down=False): # pragma: nocover (covered in autopkgtest) devdir = os.path.join('/sys/class/net', interface) if only_down: diff --git a/tests/integration/base.py b/tests/integration/base.py index 16fd2ee..7d36198 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -352,7 +352,9 @@ class IntegrationTestsBase(unittest.TestCase): '''Generate config, launch and settle NM and networkd''' # regenerate netplan config - subprocess.check_call(['netplan', 'apply']) + out = subprocess.check_output(['netplan', 'apply'], universal_newlines=True) + if 'Run \'systemctl daemon-reload\' to reload units.' in out: + self.fail('systemd units changed without reload') # start NM so that we can verify that it does not manage anything subprocess.check_call(['systemctl', 'start', '--no-block', 'NetworkManager.service']) # wait until networkd is done -- cgit v1.2.3 From f029c0a5997898825e2d2883a958c9c7186e62af Mon Sep 17 00:00:00 2001 From: Lukas Maerdian Date: Thu, 18 Jun 2020 11:28:28 +0200 Subject: [PATCH] Fix autopkgtest on arm64 with NM 1.24 (#146) On arm64 we had a test failure in tests/integration/ethernets.py -> test_manual_addresses, which was caused by the fact that netplan apply was disconnecting ALL available network interfaces via nmcli, in order to reload the new netplan config. This way the IP of the 2nd test DHCP server (dnsmasq on veth43) was lost and did not provide correct responses for the rest of the test. It is not necessary to take all interfaces down, only the ones which are (or were) configured via netplan. This was fixed in the netplan apply command. Note: This should make its way into the Groovy package as a distro-patch, to avoid future autopkgtest failures. Commits: * tests: fix ethernets/test_manual_addresses on arm64 * netplan:apply: disconnect/re-configure only netplan-NM interfaces * tests:ethernets: remove deprecated fix, was fixed via 'netplan apply' * tests: add unit-test for python utils * utils: get rid of default glob in nm_interfaces Gbp-Pq: Name 0004-Fix-autopkgtest-on-arm64-with-NM-1.24-146.patch --- netplan/cli/commands/apply.py | 11 ++++++++-- netplan/cli/utils.py | 14 ++++++++++++ tests/integration/ethernets.py | 2 ++ tests/test_utils.py | 49 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 tests/test_utils.py diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index 33ff5b5..a38fe88 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -74,7 +74,9 @@ class NetplanApply(utils.NetplanCommand): return old_files_networkd = bool(glob.glob('/run/systemd/network/*netplan-*')) - old_files_nm = bool(glob.glob('/run/NetworkManager/system-connections/netplan-*')) + old_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*') + nm_ifaces = utils.nm_interfaces(old_nm_glob) + old_files_nm = bool(old_nm_glob) generator_call = [] generate_out = None @@ -101,7 +103,10 @@ class NetplanApply(utils.NetplanCommand): 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-*')) + + restart_nm_glob = glob.glob('/run/NetworkManager/system-connections/netplan-*') + nm_ifaces += utils.nm_interfaces(restart_nm_glob) + restart_nm = bool(restart_nm_glob) if not restart_nm and old_files_nm: restart_nm = True @@ -127,6 +132,8 @@ class NetplanApply(utils.NetplanCommand): 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]) diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py index 85342fc..4f7e266 100644 --- a/netplan/cli/utils.py +++ b/netplan/cli/utils.py @@ -23,6 +23,7 @@ import fnmatch import argparse import subprocess import netifaces +import re NM_SERVICE_NAME = 'NetworkManager.service' NM_SNAP_SERVICE_NAME = 'snap.network-manager.networkmanager.service' @@ -55,6 +56,19 @@ def nm_running(): # pragma: nocover (covered in autopkgtest) return False +def nm_interfaces(paths): + pat = re.compile('^interface-name=(.*)$') + interfaces = [] + for path in paths: + with open(path, 'r') as f: + for line in f: + m = pat.match(line) + if m: + interfaces.append(m.group(1)) + break # skip to next file + return interfaces + + def systemctl_network_manager(action, sync=False): # pragma: nocover (covered in autopkgtest) service_name = NM_SERVICE_NAME diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 231d337..dc5783b 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -156,6 +156,8 @@ class _CommonTests(): ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.start_dnsmasq(None, self.dev_e2_ap) self.generate_and_settle() + if self.backend == 'NetworkManager': + self.nm_online_full(self.dev_e2_client) self.assert_iface_up(self.dev_e_client, ['inet 172.16.5.3/20'], ['inet 192.168.5', # old DHCP diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..2c137d4 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import unittest +import tempfile +import glob + +import netplan.cli.utils as utils + + +class TestUtils(unittest.TestCase): + + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + os.makedirs(os.path.join(self.workdir.name, 'etc/netplan')) + os.makedirs(os.path.join(self.workdir.name, + 'run/NetworkManager/system-connections')) + + def _create_nm_keyfile(self, filename, ifname): + with open(os.path.join(self.workdir.name, + 'run/NetworkManager/system-connections/', filename), 'w') as f: + f.write('[connection]\n') + f.write('key=value\n') + f.write('interface-name=%s\n' % ifname) + f.write('key2=value2\n') + + def test_nm_interfaces(self): + self._create_nm_keyfile('netplan-test.nmconnection', 'eth0') + self._create_nm_keyfile('netplan-test2.nmconnection', 'eth1') + ifaces = utils.nm_interfaces(glob.glob(os.path.join(self.workdir.name, + 'run/NetworkManager/system-connections/*.nmconnection'))) + self.assertTrue('eth0' in ifaces) + self.assertTrue('eth1' in ifaces) + self.assertTrue(len(ifaces) == 2) -- cgit v1.2.3 From 8f6e47a01516783b2dad89b65d6d5177ccde3b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 16 Dec 2020 13:29:29 +0100 Subject: Fix changing of macaddress with systemd v247 (#178) * networkd: Fix changing of macaddress with systemd v247 * networkd: avoid writing MACAddress= [Link] into .link file (we already have it in .network file) * network: some cleanup Gbp-Pq: Name 0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch --- src/networkd.c | 13 +++++-------- tests/generator/test_bridges.py | 3 +++ tests/generator/test_ethernets.py | 12 +++--------- tests/generator/test_vlans.py | 3 ++- tests/integration/ethernets.py | 2 +- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/networkd.c b/src/networkd.c index 7c86cd6..380095a 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -227,7 +227,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char return; /* do we need to write a .link file? */ - if (!def->set_name && !def->wake_on_lan && !def->mtubytes && !def->set_mac) + if (!def->set_name && !def->wake_on_lan && !def->mtubytes) return; /* build file contents */ @@ -241,9 +241,6 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char g_string_append_printf(s, "WakeOnLan=%s\n", def->wake_on_lan ? "magic" : "off"); if (def->mtubytes) g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); - if (def->set_mac) - g_string_append_printf(s, "MACAddress=%s\n", def->set_mac); - orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".link"); @@ -569,13 +566,13 @@ write_network_file(const NetplanNetDefinition* def, const char* rootdir, const c } } - if (def->mtubytes) { + if (def->mtubytes) g_string_append_printf(link, "MTUBytes=%u\n", def->mtubytes); - } + if (def->set_mac) + g_string_append_printf(link, "MACAddress=%s\n", def->set_mac); - if (def->emit_lldp) { + if (def->emit_lldp) g_string_append(network, "EmitLLDP=true\n"); - } if (def->dhcp4 && def->dhcp6) g_string_append(network, "DHCP=yes\n"); diff --git a/tests/generator/test_bridges.py b/tests/generator/test_bridges.py index b751074..ea048cf 100644 --- a/tests/generator/test_bridges.py +++ b/tests/generator/test_bridges.py @@ -35,6 +35,9 @@ class TestNetworkd(TestBase): self.assert_networkd({'br0.network': '''[Match] Name=br0 +[Link] +MACAddress=00:01:02:03:04:05 + [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py index 963aca1..a8be4ac 100644 --- a/tests/generator/test_ethernets.py +++ b/tests/generator/test_ethernets.py @@ -225,8 +225,8 @@ unmanaged-devices+=interface-name:green,''') macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'def1.network': ND_DHCP4 % 'green', - 'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nWakeOnLan=off\nMACAddress=00:01:02:03:04:05\n' + self.assert_networkd({'def1.network': (ND_DHCP4 % 'green') + .replace('[Network]', '[Link]\nMACAddress=00:01:02:03:04:05\n\n[Network]') }) self.assert_networkd_udev(None) @@ -442,13 +442,7 @@ method=ignore macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'eth0.link': '''[Match] -OriginalName=eth0 - -[Link] -WakeOnLan=off -MACAddress=00:01:02:03:04:05 -'''}) + self.assert_networkd(None) self.assert_nm({'eth0': '''[connection] id=netplan-eth0 diff --git a/tests/generator/test_vlans.py b/tests/generator/test_vlans.py index 606a0b1..85c5fca 100644 --- a/tests/generator/test_vlans.py +++ b/tests/generator/test_vlans.py @@ -62,7 +62,8 @@ Kind=vlan Id=3 ''', 'enblue.network': ND_WITHIP % ('enblue', '1.2.3.4/24'), - 'enred.network': ND_EMPTY % ('enred', 'ipv6'), + 'enred.network': (ND_EMPTY % ('enred', 'ipv6')) + .replace('[Network]', '[Link]\nMACAddress=aa:bb:cc:dd:ee:11\n\n[Network]'), 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) self.assert_nm(None, '''[keyfile] diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 74d4129..a362f2e 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -72,7 +72,7 @@ class _CommonTests(): ['master']) out = subprocess.check_output(['ip', 'link', 'show', self.dev_e2_client], universal_newlines=True) - self.assertTrue('ether 00:01:02:03:04:05' in out) + self.assertIn('ether 00:01:02:03:04:05', out) subprocess.check_call(['ip', 'link', 'set', self.dev_e2_client, 'address', self.dev_e2_client_mac]) -- cgit v1.2.3 From 2adeb72ebf72ef320820142ca2adce305824dc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 16 Dec 2020 18:19:46 +0100 Subject: parse: fix 'networkmanager:' backend options for modem connections (#179) The COMMON_BACKEND_HANDLERS have been forgotten for modem connections apparently. Add them to allow the definition of the special networkmanager: mapping, used for NetworkManager integration. We do not (yet) use that information (like uuid) in the current implementation. But reading YAML via NetworkManager will be broken if the networkmanager: mapping is not accepted. Gbp-Pq: Name 0002-parse-fix-networkmanager-backend-options-for-modem-c.patch --- src/parse.c | 1 + tests/generator/test_modems.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/parse.c b/src/parse.c index 033c657..f1f6a6f 100644 --- a/src/parse.c +++ b/src/parse.c @@ -2222,6 +2222,7 @@ static const mapping_entry_handler vlan_def_handlers[] = { static const mapping_entry_handler modem_def_handlers[] = { COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, {"apn", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.apn)}, {"auto-config", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(modem_params.auto_config)}, {"device-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.device_id)}, diff --git a/tests/generator/test_modems.py b/tests/generator/test_modems.py index 027aa65..ca68ddc 100644 --- a/tests/generator/test_modems.py +++ b/tests/generator/test_modems.py @@ -367,6 +367,35 @@ wake-on-lan=0 [ipv4] method=link-local +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_modem_nm_integration(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + auto-config: true + networkmanager: + uuid: b22d8f0f-3f34-46bd-ac28-801fa87f1eb6''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + [ipv6] method=ignore '''}) -- cgit v1.2.3 From ab0fd4fa16d69cd317dcdb4a5f777e674100c30d Mon Sep 17 00:00:00 2001 From: Michael Biebl Date: Mon, 4 Jan 2021 19:20:28 +0100 Subject: [PATCH] Stop using deprecated systemd-resolve tool Closes: #979266 Gbp-Pq: Name 0003-Stop-using-deprecated-systemd-resolve-tool.patch --- tests/integration/ethernets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index a362f2e..66b228c 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -152,7 +152,7 @@ class _CommonTests(): elif resolved_in_use(): sys.stdout.write('[resolved] ') sys.stdout.flush() - out = subprocess.check_output(['systemd-resolve', '--status'], universal_newlines=True) + out = subprocess.check_output(['resolvectl', 'status'], universal_newlines=True) self.assertIn('DNS Servers: 172.1.2.3', out) self.assertIn('fakesuffix', out) else: -- cgit v1.2.3 From 228252e212ee6898c49d91ab8908556d9af68504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 16 Dec 2020 13:29:29 +0100 Subject: Fix changing of macaddress with systemd v247 (#178) * networkd: Fix changing of macaddress with systemd v247 * networkd: avoid writing MACAddress= [Link] into .link file (we already have it in .network file) * network: some cleanup Gbp-Pq: Name 0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch --- src/networkd.c | 13 +++++-------- tests/generator/test_bridges.py | 3 +++ tests/generator/test_ethernets.py | 12 +++--------- tests/generator/test_vlans.py | 3 ++- tests/integration/ethernets.py | 2 +- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/networkd.c b/src/networkd.c index 7c86cd6..380095a 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -227,7 +227,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char return; /* do we need to write a .link file? */ - if (!def->set_name && !def->wake_on_lan && !def->mtubytes && !def->set_mac) + if (!def->set_name && !def->wake_on_lan && !def->mtubytes) return; /* build file contents */ @@ -241,9 +241,6 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char g_string_append_printf(s, "WakeOnLan=%s\n", def->wake_on_lan ? "magic" : "off"); if (def->mtubytes) g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); - if (def->set_mac) - g_string_append_printf(s, "MACAddress=%s\n", def->set_mac); - orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".link"); @@ -569,13 +566,13 @@ write_network_file(const NetplanNetDefinition* def, const char* rootdir, const c } } - if (def->mtubytes) { + if (def->mtubytes) g_string_append_printf(link, "MTUBytes=%u\n", def->mtubytes); - } + if (def->set_mac) + g_string_append_printf(link, "MACAddress=%s\n", def->set_mac); - if (def->emit_lldp) { + if (def->emit_lldp) g_string_append(network, "EmitLLDP=true\n"); - } if (def->dhcp4 && def->dhcp6) g_string_append(network, "DHCP=yes\n"); diff --git a/tests/generator/test_bridges.py b/tests/generator/test_bridges.py index b751074..ea048cf 100644 --- a/tests/generator/test_bridges.py +++ b/tests/generator/test_bridges.py @@ -35,6 +35,9 @@ class TestNetworkd(TestBase): self.assert_networkd({'br0.network': '''[Match] Name=br0 +[Link] +MACAddress=00:01:02:03:04:05 + [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py index 963aca1..a8be4ac 100644 --- a/tests/generator/test_ethernets.py +++ b/tests/generator/test_ethernets.py @@ -225,8 +225,8 @@ unmanaged-devices+=interface-name:green,''') macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'def1.network': ND_DHCP4 % 'green', - 'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nWakeOnLan=off\nMACAddress=00:01:02:03:04:05\n' + self.assert_networkd({'def1.network': (ND_DHCP4 % 'green') + .replace('[Network]', '[Link]\nMACAddress=00:01:02:03:04:05\n\n[Network]') }) self.assert_networkd_udev(None) @@ -442,13 +442,7 @@ method=ignore macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'eth0.link': '''[Match] -OriginalName=eth0 - -[Link] -WakeOnLan=off -MACAddress=00:01:02:03:04:05 -'''}) + self.assert_networkd(None) self.assert_nm({'eth0': '''[connection] id=netplan-eth0 diff --git a/tests/generator/test_vlans.py b/tests/generator/test_vlans.py index 606a0b1..85c5fca 100644 --- a/tests/generator/test_vlans.py +++ b/tests/generator/test_vlans.py @@ -62,7 +62,8 @@ Kind=vlan Id=3 ''', 'enblue.network': ND_WITHIP % ('enblue', '1.2.3.4/24'), - 'enred.network': ND_EMPTY % ('enred', 'ipv6'), + 'enred.network': (ND_EMPTY % ('enred', 'ipv6')) + .replace('[Network]', '[Link]\nMACAddress=aa:bb:cc:dd:ee:11\n\n[Network]'), 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) self.assert_nm(None, '''[keyfile] diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 74d4129..a362f2e 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -72,7 +72,7 @@ class _CommonTests(): ['master']) out = subprocess.check_output(['ip', 'link', 'show', self.dev_e2_client], universal_newlines=True) - self.assertTrue('ether 00:01:02:03:04:05' in out) + self.assertIn('ether 00:01:02:03:04:05', out) subprocess.check_call(['ip', 'link', 'set', self.dev_e2_client, 'address', self.dev_e2_client_mac]) -- cgit v1.2.3 From 4337b422d3317948359b628c7536ca288af3dce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 16 Dec 2020 18:19:46 +0100 Subject: parse: fix 'networkmanager:' backend options for modem connections (#179) The COMMON_BACKEND_HANDLERS have been forgotten for modem connections apparently. Add them to allow the definition of the special networkmanager: mapping, used for NetworkManager integration. We do not (yet) use that information (like uuid) in the current implementation. But reading YAML via NetworkManager will be broken if the networkmanager: mapping is not accepted. Gbp-Pq: Name 0002-parse-fix-networkmanager-backend-options-for-modem-c.patch --- src/parse.c | 1 + tests/generator/test_modems.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/parse.c b/src/parse.c index 033c657..f1f6a6f 100644 --- a/src/parse.c +++ b/src/parse.c @@ -2222,6 +2222,7 @@ static const mapping_entry_handler vlan_def_handlers[] = { static const mapping_entry_handler modem_def_handlers[] = { COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, {"apn", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.apn)}, {"auto-config", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(modem_params.auto_config)}, {"device-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.device_id)}, diff --git a/tests/generator/test_modems.py b/tests/generator/test_modems.py index 027aa65..ca68ddc 100644 --- a/tests/generator/test_modems.py +++ b/tests/generator/test_modems.py @@ -367,6 +367,35 @@ wake-on-lan=0 [ipv4] method=link-local +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_modem_nm_integration(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + auto-config: true + networkmanager: + uuid: b22d8f0f-3f34-46bd-ac28-801fa87f1eb6''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + [ipv6] method=ignore '''}) -- cgit v1.2.3 From fcd4f2bd92502e689a1fe1c03b122ddf67d88eb9 Mon Sep 17 00:00:00 2001 From: Michael Biebl Date: Mon, 4 Jan 2021 19:20:28 +0100 Subject: [PATCH] Stop using deprecated systemd-resolve tool Closes: #979266 Gbp-Pq: Name 0003-Stop-using-deprecated-systemd-resolve-tool.patch --- tests/integration/ethernets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index a362f2e..66b228c 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -152,7 +152,7 @@ class _CommonTests(): elif resolved_in_use(): sys.stdout.write('[resolved] ') sys.stdout.flush() - out = subprocess.check_output(['systemd-resolve', '--status'], universal_newlines=True) + out = subprocess.check_output(['resolvectl', 'status'], universal_newlines=True) self.assertIn('DNS Servers: 172.1.2.3', out) self.assertIn('fakesuffix', out) else: -- cgit v1.2.3 From d7913e024f9905210c8ac66e0408a39fb3e8edad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 16 Dec 2020 13:29:29 +0100 Subject: Fix changing of macaddress with systemd v247 (#178) * networkd: Fix changing of macaddress with systemd v247 * networkd: avoid writing MACAddress= [Link] into .link file (we already have it in .network file) * network: some cleanup Gbp-Pq: Name 0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch --- src/networkd.c | 13 +++++-------- tests/generator/test_bridges.py | 3 +++ tests/generator/test_ethernets.py | 12 +++--------- tests/generator/test_vlans.py | 3 ++- tests/integration/ethernets.py | 2 +- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/networkd.c b/src/networkd.c index 7c86cd6..380095a 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -227,7 +227,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char return; /* do we need to write a .link file? */ - if (!def->set_name && !def->wake_on_lan && !def->mtubytes && !def->set_mac) + if (!def->set_name && !def->wake_on_lan && !def->mtubytes) return; /* build file contents */ @@ -241,9 +241,6 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char g_string_append_printf(s, "WakeOnLan=%s\n", def->wake_on_lan ? "magic" : "off"); if (def->mtubytes) g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); - if (def->set_mac) - g_string_append_printf(s, "MACAddress=%s\n", def->set_mac); - orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".link"); @@ -569,13 +566,13 @@ write_network_file(const NetplanNetDefinition* def, const char* rootdir, const c } } - if (def->mtubytes) { + if (def->mtubytes) g_string_append_printf(link, "MTUBytes=%u\n", def->mtubytes); - } + if (def->set_mac) + g_string_append_printf(link, "MACAddress=%s\n", def->set_mac); - if (def->emit_lldp) { + if (def->emit_lldp) g_string_append(network, "EmitLLDP=true\n"); - } if (def->dhcp4 && def->dhcp6) g_string_append(network, "DHCP=yes\n"); diff --git a/tests/generator/test_bridges.py b/tests/generator/test_bridges.py index b751074..ea048cf 100644 --- a/tests/generator/test_bridges.py +++ b/tests/generator/test_bridges.py @@ -35,6 +35,9 @@ class TestNetworkd(TestBase): self.assert_networkd({'br0.network': '''[Match] Name=br0 +[Link] +MACAddress=00:01:02:03:04:05 + [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py index 963aca1..a8be4ac 100644 --- a/tests/generator/test_ethernets.py +++ b/tests/generator/test_ethernets.py @@ -225,8 +225,8 @@ unmanaged-devices+=interface-name:green,''') macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'def1.network': ND_DHCP4 % 'green', - 'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nWakeOnLan=off\nMACAddress=00:01:02:03:04:05\n' + self.assert_networkd({'def1.network': (ND_DHCP4 % 'green') + .replace('[Network]', '[Link]\nMACAddress=00:01:02:03:04:05\n\n[Network]') }) self.assert_networkd_udev(None) @@ -442,13 +442,7 @@ method=ignore macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'eth0.link': '''[Match] -OriginalName=eth0 - -[Link] -WakeOnLan=off -MACAddress=00:01:02:03:04:05 -'''}) + self.assert_networkd(None) self.assert_nm({'eth0': '''[connection] id=netplan-eth0 diff --git a/tests/generator/test_vlans.py b/tests/generator/test_vlans.py index 606a0b1..85c5fca 100644 --- a/tests/generator/test_vlans.py +++ b/tests/generator/test_vlans.py @@ -62,7 +62,8 @@ Kind=vlan Id=3 ''', 'enblue.network': ND_WITHIP % ('enblue', '1.2.3.4/24'), - 'enred.network': ND_EMPTY % ('enred', 'ipv6'), + 'enred.network': (ND_EMPTY % ('enred', 'ipv6')) + .replace('[Network]', '[Link]\nMACAddress=aa:bb:cc:dd:ee:11\n\n[Network]'), 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) self.assert_nm(None, '''[keyfile] diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 74d4129..a362f2e 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -72,7 +72,7 @@ class _CommonTests(): ['master']) out = subprocess.check_output(['ip', 'link', 'show', self.dev_e2_client], universal_newlines=True) - self.assertTrue('ether 00:01:02:03:04:05' in out) + self.assertIn('ether 00:01:02:03:04:05', out) subprocess.check_call(['ip', 'link', 'set', self.dev_e2_client, 'address', self.dev_e2_client_mac]) -- cgit v1.2.3 From 4ee24b7dff18df604268472f7753bd94a26ae53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 16 Dec 2020 18:19:46 +0100 Subject: parse: fix 'networkmanager:' backend options for modem connections (#179) The COMMON_BACKEND_HANDLERS have been forgotten for modem connections apparently. Add them to allow the definition of the special networkmanager: mapping, used for NetworkManager integration. We do not (yet) use that information (like uuid) in the current implementation. But reading YAML via NetworkManager will be broken if the networkmanager: mapping is not accepted. Gbp-Pq: Name 0002-parse-fix-networkmanager-backend-options-for-modem-c.patch --- src/parse.c | 1 + tests/generator/test_modems.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/parse.c b/src/parse.c index 033c657..f1f6a6f 100644 --- a/src/parse.c +++ b/src/parse.c @@ -2222,6 +2222,7 @@ static const mapping_entry_handler vlan_def_handlers[] = { static const mapping_entry_handler modem_def_handlers[] = { COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, {"apn", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.apn)}, {"auto-config", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(modem_params.auto_config)}, {"device-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.device_id)}, diff --git a/tests/generator/test_modems.py b/tests/generator/test_modems.py index 027aa65..ca68ddc 100644 --- a/tests/generator/test_modems.py +++ b/tests/generator/test_modems.py @@ -367,6 +367,35 @@ wake-on-lan=0 [ipv4] method=link-local +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_modem_nm_integration(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + auto-config: true + networkmanager: + uuid: b22d8f0f-3f34-46bd-ac28-801fa87f1eb6''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + [ipv6] method=ignore '''}) -- cgit v1.2.3 From 5c5b2da96333a1d0f792047f096bc9bf499033a4 Mon Sep 17 00:00:00 2001 From: Michael Biebl Date: Mon, 4 Jan 2021 19:20:28 +0100 Subject: [PATCH] Stop using deprecated systemd-resolve tool Closes: #979266 Gbp-Pq: Name 0003-Stop-using-deprecated-systemd-resolve-tool.patch --- tests/integration/ethernets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index a362f2e..66b228c 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -152,7 +152,7 @@ class _CommonTests(): elif resolved_in_use(): sys.stdout.write('[resolved] ') sys.stdout.flush() - out = subprocess.check_output(['systemd-resolve', '--status'], universal_newlines=True) + out = subprocess.check_output(['resolvectl', 'status'], universal_newlines=True) self.assertIn('DNS Servers: 172.1.2.3', out) self.assertIn('fakesuffix', out) else: -- cgit v1.2.3 From e93856de07058f8c53dd5961021d732b2aa59f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 16 Dec 2020 13:29:29 +0100 Subject: Fix changing of macaddress with systemd v247 (#178) * networkd: Fix changing of macaddress with systemd v247 * networkd: avoid writing MACAddress= [Link] into .link file (we already have it in .network file) * network: some cleanup Gbp-Pq: Name 0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch --- src/networkd.c | 13 +++++-------- tests/generator/test_bridges.py | 3 +++ tests/generator/test_ethernets.py | 12 +++--------- tests/generator/test_vlans.py | 3 ++- tests/integration/ethernets.py | 2 +- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/networkd.c b/src/networkd.c index 7c86cd6..380095a 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -227,7 +227,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char return; /* do we need to write a .link file? */ - if (!def->set_name && !def->wake_on_lan && !def->mtubytes && !def->set_mac) + if (!def->set_name && !def->wake_on_lan && !def->mtubytes) return; /* build file contents */ @@ -241,9 +241,6 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char g_string_append_printf(s, "WakeOnLan=%s\n", def->wake_on_lan ? "magic" : "off"); if (def->mtubytes) g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); - if (def->set_mac) - g_string_append_printf(s, "MACAddress=%s\n", def->set_mac); - orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".link"); @@ -569,13 +566,13 @@ write_network_file(const NetplanNetDefinition* def, const char* rootdir, const c } } - if (def->mtubytes) { + if (def->mtubytes) g_string_append_printf(link, "MTUBytes=%u\n", def->mtubytes); - } + if (def->set_mac) + g_string_append_printf(link, "MACAddress=%s\n", def->set_mac); - if (def->emit_lldp) { + if (def->emit_lldp) g_string_append(network, "EmitLLDP=true\n"); - } if (def->dhcp4 && def->dhcp6) g_string_append(network, "DHCP=yes\n"); diff --git a/tests/generator/test_bridges.py b/tests/generator/test_bridges.py index b751074..ea048cf 100644 --- a/tests/generator/test_bridges.py +++ b/tests/generator/test_bridges.py @@ -35,6 +35,9 @@ class TestNetworkd(TestBase): self.assert_networkd({'br0.network': '''[Match] Name=br0 +[Link] +MACAddress=00:01:02:03:04:05 + [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py index 963aca1..a8be4ac 100644 --- a/tests/generator/test_ethernets.py +++ b/tests/generator/test_ethernets.py @@ -225,8 +225,8 @@ unmanaged-devices+=interface-name:green,''') macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'def1.network': ND_DHCP4 % 'green', - 'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nWakeOnLan=off\nMACAddress=00:01:02:03:04:05\n' + self.assert_networkd({'def1.network': (ND_DHCP4 % 'green') + .replace('[Network]', '[Link]\nMACAddress=00:01:02:03:04:05\n\n[Network]') }) self.assert_networkd_udev(None) @@ -442,13 +442,7 @@ method=ignore macaddress: 00:01:02:03:04:05 dhcp4: true''') - self.assert_networkd({'eth0.link': '''[Match] -OriginalName=eth0 - -[Link] -WakeOnLan=off -MACAddress=00:01:02:03:04:05 -'''}) + self.assert_networkd(None) self.assert_nm({'eth0': '''[connection] id=netplan-eth0 diff --git a/tests/generator/test_vlans.py b/tests/generator/test_vlans.py index 606a0b1..85c5fca 100644 --- a/tests/generator/test_vlans.py +++ b/tests/generator/test_vlans.py @@ -62,7 +62,8 @@ Kind=vlan Id=3 ''', 'enblue.network': ND_WITHIP % ('enblue', '1.2.3.4/24'), - 'enred.network': ND_EMPTY % ('enred', 'ipv6'), + 'enred.network': (ND_EMPTY % ('enred', 'ipv6')) + .replace('[Network]', '[Link]\nMACAddress=aa:bb:cc:dd:ee:11\n\n[Network]'), 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) self.assert_nm(None, '''[keyfile] diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 74d4129..a362f2e 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -72,7 +72,7 @@ class _CommonTests(): ['master']) out = subprocess.check_output(['ip', 'link', 'show', self.dev_e2_client], universal_newlines=True) - self.assertTrue('ether 00:01:02:03:04:05' in out) + self.assertIn('ether 00:01:02:03:04:05', out) subprocess.check_call(['ip', 'link', 'set', self.dev_e2_client, 'address', self.dev_e2_client_mac]) -- cgit v1.2.3 From dc1c96e96445f573ed49cda55f7d762356c01cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 16 Dec 2020 18:19:46 +0100 Subject: parse: fix 'networkmanager:' backend options for modem connections (#179) The COMMON_BACKEND_HANDLERS have been forgotten for modem connections apparently. Add them to allow the definition of the special networkmanager: mapping, used for NetworkManager integration. We do not (yet) use that information (like uuid) in the current implementation. But reading YAML via NetworkManager will be broken if the networkmanager: mapping is not accepted. Gbp-Pq: Name 0002-parse-fix-networkmanager-backend-options-for-modem-c.patch --- src/parse.c | 1 + tests/generator/test_modems.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/parse.c b/src/parse.c index 033c657..f1f6a6f 100644 --- a/src/parse.c +++ b/src/parse.c @@ -2222,6 +2222,7 @@ static const mapping_entry_handler vlan_def_handlers[] = { static const mapping_entry_handler modem_def_handlers[] = { COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, {"apn", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.apn)}, {"auto-config", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(modem_params.auto_config)}, {"device-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.device_id)}, diff --git a/tests/generator/test_modems.py b/tests/generator/test_modems.py index 027aa65..ca68ddc 100644 --- a/tests/generator/test_modems.py +++ b/tests/generator/test_modems.py @@ -367,6 +367,35 @@ wake-on-lan=0 [ipv4] method=link-local +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_modem_nm_integration(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + auto-config: true + networkmanager: + uuid: b22d8f0f-3f34-46bd-ac28-801fa87f1eb6''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + [ipv6] method=ignore '''}) -- cgit v1.2.3 From 1b513bb19eb3194fe44f0fbe901de28dd181a1f7 Mon Sep 17 00:00:00 2001 From: Michael Biebl Date: Mon, 4 Jan 2021 19:20:28 +0100 Subject: [PATCH] Stop using deprecated systemd-resolve tool Closes: #979266 Gbp-Pq: Name 0003-Stop-using-deprecated-systemd-resolve-tool.patch --- tests/integration/ethernets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index a362f2e..66b228c 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -152,7 +152,7 @@ class _CommonTests(): elif resolved_in_use(): sys.stdout.write('[resolved] ') sys.stdout.flush() - out = subprocess.check_output(['systemd-resolve', '--status'], universal_newlines=True) + out = subprocess.check_output(['resolvectl', 'status'], universal_newlines=True) self.assertIn('DNS Servers: 172.1.2.3', out) self.assertIn('fakesuffix', out) else: -- cgit v1.2.3 From 4887fc49981b1a0848874b725eba56efc4ac0cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 4 Aug 2021 15:15:45 +0200 Subject: parse-nm: fix 32bit format string Gbp-Pq: Name 0001-parse-nm-fix-32bit-format-string.patch --- src/parse-nm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parse-nm.c b/src/parse-nm.c index 9b09e34..bf998b7 100644 --- a/src/parse-nm.c +++ b/src/parse-nm.c @@ -136,7 +136,7 @@ static void handle_bridge_uint(GKeyFile* kf, const gchar* key, NetplanNetDefinition* nd, char** dataptr) { if (g_key_file_get_uint64(kf, "bridge", key, NULL)) { nd->custom_bridging = TRUE; - *dataptr = g_strdup_printf("%lu", g_key_file_get_uint64(kf, "bridge", key, NULL)); + *dataptr = g_strdup_printf("%"G_GUINT64_FORMAT, g_key_file_get_uint64(kf, "bridge", key, NULL)); _kf_clear_key(kf, "bridge", key); } } -- cgit v1.2.3 From 387ca006fe657a728eba1b39307208b668058fb6 Mon Sep 17 00:00:00 2001 From: Debian netplan Maintainers Date: Wed, 20 Oct 2021 13:22:07 +0200 Subject: autopkgtest-fixes Gbp-Pq: Name autopkgtest-fixes.patch --- tests/integration/base.py | 11 +++-------- tests/integration/ethernets.py | 5 ++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index 5042bf4..93ee722 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -75,7 +75,7 @@ class IntegrationTestsBase(unittest.TestCase): os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') + f.write('[keyfile]\nunmanaged-devices+=interface-name:en*,eth0,veth42,veth43,nptestsrv') subprocess.check_call(['netplan', 'apply']) subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) @@ -144,12 +144,6 @@ class IntegrationTestsBase(unittest.TestCase): universal_newlines=True) klass.dev_e2_client_mac = out.split()[2] - os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) - - # work around https://launchpad.net/bugs/1615044 - with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices=') - @classmethod def shutdown_devices(klass): '''Remove test devices''' @@ -432,9 +426,10 @@ class IntegrationTestsWifi(IntegrationTestsBase): klass.dev_w_ap = devs[0] klass.dev_w_client = devs[1] + os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) # don't let NM trample over our fake AP with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: - f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=%s\n' % klass.dev_w_ap) @classmethod def shutdown_devices(klass): diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index ce016da..865c0d4 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -252,9 +252,8 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests): %(ec)s: dhcp6: no accept-ra: yes - addresses: [ '192.168.1.100/24' ] - %(e2c)s: {}''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() + addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], []) def test_eth_dhcp6_off_no_accept_ra(self): -- cgit v1.2.3 From abd2301ba688c36966973929a0da4942b6b7935f Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Wed, 20 Oct 2021 13:22:07 +0200 Subject: Fix ethernets test with network-manage 1.32.10 Origin: vendor Forwarded: no Last-Update: 2021-09-01 NetworkManager's udev rules are smarter about catching renamed veths now, so the udev rule the integration tests use to manage their veths need to list the names the veths are renamed to too. Last-Update: 2021-09-01 Gbp-Pq: Name nm-1.32.10-compat.patch --- tests/integration/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index 93ee722..1054059 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -70,7 +70,7 @@ class IntegrationTestsBase(unittest.TestCase): # ensure NM can manage our fake eths os.makedirs('/run/udev/rules.d', exist_ok=True) with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: - f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') + f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43|iface1|iface2", ENV{NM_UNMANAGED}="0"\n') subprocess.check_call(['udevadm', 'control', '--reload']) os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) -- cgit v1.2.3 From 48c20e9eab47968d8769a36faf27b358d116d3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Fri, 1 Oct 2021 12:39:40 +0200 Subject: generate:dbus:util: glib 2.70 compat g_spawn_check_exit_status is deprecated since libglib 2.70 and replaced by g_spawn_check_wait_status Gbp-Pq: Name glib-2.70-compat.patch --- src/dbus.c | 10 +++++----- src/generate.c | 2 +- src/util.c | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dbus.c b/src/dbus.c index f0aa53a..74032dc 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -110,7 +110,7 @@ _try_accept(bool accept, sd_bus_message *m, NetplanData *d, sd_bus_error *ret_er * Check return code/errors. */ kill(d->try_pid, signal); waitpid(d->try_pid, &status, 0); - g_spawn_check_exit_status(status, &error); + g_spawn_check_wait_status(status, &error); if (error != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan try failed: %s", error->message); // LCOV_EXCL_LINE @@ -228,7 +228,7 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan apply: %s", err->message); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan apply failed: %s\nstdout: '%s'\nstderr: '%s'", @@ -257,7 +257,7 @@ method_generate(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan generate: %s", err->message); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan generate failed: %s\nstdout: '%s'\nstderr: '%s'", @@ -334,7 +334,7 @@ method_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan get: %s", err->message); // LCOV_EXCL_LINE - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan get failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE @@ -380,7 +380,7 @@ method_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan set %s: %s", config_delta, err->message); // LCOV_EXCL_LINE - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan set failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE diff --git a/src/generate.c b/src/generate.c index cccd47a..24e60a2 100644 --- a/src/generate.c +++ b/src/generate.c @@ -67,7 +67,7 @@ check_called_just_in_time() gint exit_code = 0; g_spawn_sync(NULL, (gchar**)argv2, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, &exit_code, NULL); /* return TRUE, if network.target is not yet active */ - return !g_spawn_check_exit_status(exit_code, NULL); + return !g_spawn_check_wait_status(exit_code, NULL); } g_free(output); return FALSE; diff --git a/src/util.c b/src/util.c index a4c0dba..5861e13 100644 --- a/src/util.c +++ b/src/util.c @@ -188,7 +188,7 @@ systemd_escape(char* string) gchar *argv[] = {"bin" "/" "systemd-escape", string, NULL}; g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &escaped, &stderrh, &exit_status, &err); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) { // LCOV_EXCL_START g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", string, exit_status, escaped, stderrh); -- cgit v1.2.3 From 8d1727ff671179f971d6ad00d029356947c930b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 20 Oct 2021 13:22:07 +0200 Subject: Import netplan.io_0.103.orig.tar.gz [dgit import orig netplan.io_0.103.orig.tar.gz] --- .github/pull_request_template.md | 12 + .github/workflows/build.yml | 33 + .github/workflows/check-coverage.yml | 41 + .github/workflows/codeql-analysis.yml | 74 + .gitignore | 12 + CONTRIBUTING | 56 + COPYING | 674 +++++ Makefile | 144 + README.md | 28 + TODO | 41 + dbus/io.netplan.Netplan.conf | 20 + dbus/io.netplan.Netplan.service.in | 5 + doc/example-config | 61 + doc/manpage-footer.md | 3 + doc/manpage-header.md | 21 + doc/netplan-apply.md | 62 + doc/netplan-dbus.md | 43 + doc/netplan-generate.md | 88 + doc/netplan-get.md | 39 + doc/netplan-set.md | 42 + doc/netplan-try.md | 64 + doc/netplan.md | 1455 ++++++++++ examples/bonding.yaml | 12 + examples/bonding_router.yaml | 46 + examples/bridge.yaml | 11 + examples/bridge_vlan.yaml | 15 + examples/dbus_config_scenario.txt | 41 + examples/dhcp.yaml | 6 + examples/dhcp_wired8021x.yaml | 11 + examples/direct_connect_gateway.yaml | 9 + examples/direct_connect_gateway_ipv6.yaml | 11 + examples/ipv6_tunnel.yaml | 20 + examples/loopback_interface.yaml | 8 + examples/modem.yaml | 15 + examples/network_manager.yaml | 3 + examples/openvswitch.yaml | 45 + examples/route_metric.yaml | 11 + examples/source_routing.yaml | 28 + examples/sriov.yaml | 14 + examples/sriov_vlan.yaml | 18 + examples/static.yaml | 13 + examples/static_multiaddress.yaml | 11 + .../static_singlenic_multiip_multigateway.yaml | 19 + examples/vlan.yaml | 27 + examples/windows_dhcp_server.yaml | 6 + examples/wireguard.yaml | 31 + examples/wireless.yaml | 16 + examples/wpa_enterprise.yaml | 26 + netplan.completions | 45 + netplan/__init__.py | 20 + netplan/cli/__init__.py | 16 + netplan/cli/commands/__init__.py | 36 + netplan/cli/commands/apply.py | 334 +++ netplan/cli/commands/generate.py | 85 + netplan/cli/commands/get.py | 67 + netplan/cli/commands/info.py | 65 + netplan/cli/commands/ip.py | 153 ++ netplan/cli/commands/migrate.py | 416 +++ netplan/cli/commands/set.py | 169 ++ netplan/cli/commands/try_command.py | 184 ++ netplan/cli/core.py | 50 + netplan/cli/ovs.py | 178 ++ netplan/cli/sriov.py | 334 +++ netplan/cli/utils.py | 297 +++ netplan/configmanager.py | 320 +++ netplan/terminal.py | 157 ++ rpm/netplan.spec | 131 + snap/snapcraft.yaml | 42 + src/dbus.c | 798 ++++++ src/error.c | 174 ++ src/error.h | 31 + src/generate.c | 295 +++ src/netplan.c | 965 +++++++ src/netplan.h | 145 + src/netplan.script | 23 + src/networkd.c | 1131 ++++++++ src/networkd.h | 26 + src/nm.c | 999 +++++++ src/nm.h | 24 + src/openvswitch.c | 484 ++++ src/openvswitch.h | 24 + src/parse-nm.c | 737 ++++++ src/parse-nm.h | 22 + src/parse.c | 2790 ++++++++++++++++++++ src/parse.h | 517 ++++ src/sriov.c | 40 + src/sriov.h | 21 + src/util.c | 329 +++ src/util.h | 41 + src/validation.c | 486 ++++ src/validation.h | 37 + tests/cli.py | 692 +++++ tests/dbus/__init__.py | 0 tests/dbus/test_dbus.py | 762 ++++++ tests/generator/__init__.py | 17 + tests/generator/base.py | 446 ++++ tests/generator/test_args.py | 184 ++ tests/generator/test_auth.py | 555 ++++ tests/generator/test_bonds.py | 812 ++++++ tests/generator/test_bridges.py | 733 +++++ tests/generator/test_common.py | 1690 ++++++++++++ tests/generator/test_dhcp_overrides.py | 426 +++ tests/generator/test_errors.py | 976 +++++++ tests/generator/test_ethernets.py | 715 +++++ tests/generator/test_modems.py | 426 +++ tests/generator/test_ovs.py | 1021 +++++++ tests/generator/test_passthrough.py | 286 ++ tests/generator/test_routing.py | 1333 ++++++++++ tests/generator/test_tunnels.py | 1409 ++++++++++ tests/generator/test_vlans.py | 306 +++ tests/generator/test_wifis.py | 692 +++++ tests/integration/__init__.py | 17 + tests/integration/base.py | 484 ++++ tests/integration/bonds.py | 675 +++++ tests/integration/bridges.py | 348 +++ tests/integration/ethernets.py | 336 +++ tests/integration/ovs.py | 557 ++++ tests/integration/regressions.py | 90 + tests/integration/routing.py | 339 +++ tests/integration/run.py | 85 + tests/integration/scenarios.py | 123 + tests/integration/tunnels.py | 221 ++ tests/integration/vlans.py | 103 + tests/integration/wifi.py | 158 ++ tests/parser/__init__.py | 17 + tests/parser/base.py | 169 ++ tests/parser/test_keyfile.py | 1018 +++++++ tests/test_cli_get_set.py | 386 +++ tests/test_cli_units.py | 41 + tests/test_configmanager.py | 251 ++ tests/test_libnetplan.py | 153 ++ tests/test_ovs.py | 148 ++ tests/test_sriov.py | 648 +++++ tests/test_terminal.py | 84 + tests/test_utils.py | 295 +++ tests/validate_docs.sh | 47 + 136 files changed, 37003 insertions(+) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/check-coverage.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING create mode 100644 COPYING create mode 100644 Makefile create mode 100644 README.md create mode 100644 TODO create mode 100644 dbus/io.netplan.Netplan.conf create mode 100644 dbus/io.netplan.Netplan.service.in create mode 100644 doc/example-config create mode 100644 doc/manpage-footer.md create mode 100644 doc/manpage-header.md create mode 100644 doc/netplan-apply.md create mode 100644 doc/netplan-dbus.md create mode 100644 doc/netplan-generate.md create mode 100644 doc/netplan-get.md create mode 100644 doc/netplan-set.md create mode 100644 doc/netplan-try.md create mode 100644 doc/netplan.md create mode 100644 examples/bonding.yaml create mode 100644 examples/bonding_router.yaml create mode 100644 examples/bridge.yaml create mode 100644 examples/bridge_vlan.yaml create mode 100644 examples/dbus_config_scenario.txt create mode 100644 examples/dhcp.yaml create mode 100644 examples/dhcp_wired8021x.yaml create mode 100644 examples/direct_connect_gateway.yaml create mode 100644 examples/direct_connect_gateway_ipv6.yaml create mode 100644 examples/ipv6_tunnel.yaml create mode 100644 examples/loopback_interface.yaml create mode 100644 examples/modem.yaml create mode 100644 examples/network_manager.yaml create mode 100644 examples/openvswitch.yaml create mode 100644 examples/route_metric.yaml create mode 100644 examples/source_routing.yaml create mode 100644 examples/sriov.yaml create mode 100644 examples/sriov_vlan.yaml create mode 100644 examples/static.yaml create mode 100644 examples/static_multiaddress.yaml create mode 100644 examples/static_singlenic_multiip_multigateway.yaml create mode 100644 examples/vlan.yaml create mode 100644 examples/windows_dhcp_server.yaml create mode 100644 examples/wireguard.yaml create mode 100644 examples/wireless.yaml create mode 100644 examples/wpa_enterprise.yaml create mode 100644 netplan.completions create mode 100644 netplan/__init__.py create mode 100644 netplan/cli/__init__.py create mode 100644 netplan/cli/commands/__init__.py create mode 100644 netplan/cli/commands/apply.py create mode 100644 netplan/cli/commands/generate.py create mode 100644 netplan/cli/commands/get.py create mode 100644 netplan/cli/commands/info.py create mode 100644 netplan/cli/commands/ip.py create mode 100644 netplan/cli/commands/migrate.py create mode 100644 netplan/cli/commands/set.py create mode 100644 netplan/cli/commands/try_command.py create mode 100644 netplan/cli/core.py create mode 100644 netplan/cli/ovs.py create mode 100644 netplan/cli/sriov.py create mode 100644 netplan/cli/utils.py create mode 100644 netplan/configmanager.py create mode 100644 netplan/terminal.py create mode 100644 rpm/netplan.spec create mode 100644 snap/snapcraft.yaml create mode 100644 src/dbus.c create mode 100644 src/error.c create mode 100644 src/error.h create mode 100644 src/generate.c create mode 100644 src/netplan.c create mode 100644 src/netplan.h create mode 100755 src/netplan.script create mode 100644 src/networkd.c create mode 100644 src/networkd.h create mode 100644 src/nm.c create mode 100644 src/nm.h create mode 100644 src/openvswitch.c create mode 100644 src/openvswitch.h create mode 100644 src/parse-nm.c create mode 100644 src/parse-nm.h create mode 100644 src/parse.c create mode 100644 src/parse.h create mode 100644 src/sriov.c create mode 100644 src/sriov.h create mode 100644 src/util.c create mode 100644 src/util.h create mode 100644 src/validation.c create mode 100644 src/validation.h create mode 100755 tests/cli.py create mode 100644 tests/dbus/__init__.py create mode 100644 tests/dbus/test_dbus.py create mode 100644 tests/generator/__init__.py create mode 100644 tests/generator/base.py create mode 100644 tests/generator/test_args.py create mode 100644 tests/generator/test_auth.py create mode 100644 tests/generator/test_bonds.py create mode 100644 tests/generator/test_bridges.py create mode 100644 tests/generator/test_common.py create mode 100644 tests/generator/test_dhcp_overrides.py create mode 100644 tests/generator/test_errors.py create mode 100644 tests/generator/test_ethernets.py create mode 100644 tests/generator/test_modems.py create mode 100644 tests/generator/test_ovs.py create mode 100644 tests/generator/test_passthrough.py create mode 100644 tests/generator/test_routing.py create mode 100644 tests/generator/test_tunnels.py create mode 100644 tests/generator/test_vlans.py create mode 100644 tests/generator/test_wifis.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/base.py create mode 100644 tests/integration/bonds.py create mode 100644 tests/integration/bridges.py create mode 100644 tests/integration/ethernets.py create mode 100644 tests/integration/ovs.py create mode 100644 tests/integration/regressions.py create mode 100644 tests/integration/routing.py create mode 100755 tests/integration/run.py create mode 100644 tests/integration/scenarios.py create mode 100644 tests/integration/tunnels.py create mode 100644 tests/integration/vlans.py create mode 100644 tests/integration/wifi.py create mode 100644 tests/parser/__init__.py create mode 100644 tests/parser/base.py create mode 100644 tests/parser/test_keyfile.py create mode 100644 tests/test_cli_get_set.py create mode 100644 tests/test_cli_units.py create mode 100644 tests/test_configmanager.py create mode 100644 tests/test_libnetplan.py create mode 100644 tests/test_ovs.py create mode 100644 tests/test_sriov.py create mode 100644 tests/test_terminal.py create mode 100644 tests/test_utils.py create mode 100755 tests/validate_docs.sh diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..435976e --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ + +## Description + + +## Checklist + +- [ ] Runs `make check` successfully. +- [ ] Retains 100% code coverage (`make check-coverage`). +- [ ] New/changed keys in YAML format are documented. +- [ ] \(Optional\) Adds example YAML for new feature. +- [ ] \(Optional\) Closes an open bug in Launchpad. + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5ecc0d9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,33 @@ +name: Build + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-20.04 + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Installs the build dependencies + - name: Install build depends + run: | + sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list + sudo apt update + #sudo apt install lcov python3-coverage curl + sudo apt build-dep netplan.io + + # Runs the build + - name: Run build + run: make diff --git a/.github/workflows/check-coverage.yml b/.github/workflows/check-coverage.yml new file mode 100644 index 0000000..c102a62 --- /dev/null +++ b/.github/workflows/check-coverage.yml @@ -0,0 +1,41 @@ +name: Unit tests & Coverage + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + branches: [ master ] + pull_request: + branches: [ '**' ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "test-and-coverage" + test-and-coverage: + # The type of runner that the job will run on + runs-on: ubuntu-20.04 + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + # Installs the build dependencies + - name: Install build depends + run: | + sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list + sudo apt update + sudo apt install lcov python3-coverage curl + sudo apt build-dep netplan.io + + # Runs the unit tests with coverage + - name: Run unit tests + run: make coverage + + # Checks the coverage diff to the master branch + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + name: check-coverage + fail_ci_if_error: true + verbose: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..2c5869e --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '17 21 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'cpp', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Installs the build dependencies + - name: Install build depends + run: | + sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list + sudo apt update + sudo apt build-dep netplan.io + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eda3d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +generate +netplan-dbus +test-coverage +doc/*.html +doc/*.[1-9] +__pycache__ +*.pyc +.coverage +.vscode +src/_features.h +netplan/_features.py +dbus/io.netplan.Netplan.service diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..9712d67 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,56 @@ +# Contributing to netplan.io + +Thanks for taking the time to contribute to netplan! + +Here are the guidelines for contributing to the development of netplan. These +are guidelines, not hard and fast rules; but please exercise judgement. Feel +free to propose changes to this document. + +#### Table Of Contents + +[Code of Conduct](#code-of-conduct) + +[What should I know before I get started](#what-should-i-know-before-i-get-started) + * [Did you find a bug?](#did-you-find-a-bug) + * [Code Quality](#code-quality) + * [Conventions](#conventions) + +## Code of Conduct + +This project and everyone participating in it is governed by the +[Ubuntu Code of Conduct](https://www.ubuntu.com/community/code-of-conduct). +By participating, you are expected to uphold this code. Please report +unacceptable behavior to +[netplan-developers@lists.launchpad.net](mailto:netplan-developers@lists.launchpad.net). + +## What should I know before I get started? + +### Did you find a bug? + +If you've found a bug, please make sure to report it on Launchpad against the +[netplan](http://bugs.launchpad.net/netplan) project. We do use these bug reports +to make it easier to backport features to supported releases of Ubuntu: having +an existing bug report, with people who understand well how the bug appears +helps immensely in making sure the feature or bug fix is well tested when it is +being released to supported Ubuntu releases. + +### Code quality + +We want to maintain the quality of the code in netplan to the highest possible +degree. As such, we do insist on keeping a code coverage with unit tests to 100% +coverage if it is possible. If not, please make sure to explain why when submitting +a pull request, and expect reviewers to challenge you on that decision and suggest +a course of action. + +### Conventions + +The netplan project mixes C and python code. Generator code is generally all +written in C, while the UI / command-line interface is written in python. Please +look at the surrounding code, and make a best effort to follow the general style +used in the code. We do insist on proper indentation (4 spaces), but we will +not block good features and bug fixes on purely style issues. Please exercise +your best judgement: if it looks odd or too clever to you, chances are it will +look odd or too clever to code reviewers. In that case, you may be asked for +some styles changes in a pull request. Similary, if you see code that you +find hard to understand, we do encourage that you submit pull requests that +help make the code easier to understand and maintain. diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8645aee --- /dev/null +++ b/Makefile @@ -0,0 +1,144 @@ +NETPLAN_SOVER=0.0 + +BUILDFLAGS = \ + -g \ + -fPIC \ + -std=c99 \ + -D_XOPEN_SOURCE=500 \ + -DSBINDIR=\"$(SBINDIR)\" \ + -Wall \ + -Werror \ + $(NULL) + +SYSTEMD_GENERATOR_DIR=$(shell pkg-config --variable=systemdsystemgeneratordir systemd) +SYSTEMD_UNIT_DIR=$(shell pkg-config --variable=systemdsystemunitdir systemd) +BASH_COMPLETIONS_DIR=$(shell pkg-config --variable=completionsdir bash-completion || echo "/etc/bash_completion.d") + +GCOV ?= gcov +ROOTPREFIX ?= +PREFIX ?= /usr +LIBDIR ?= $(PREFIX)/lib +ROOTLIBEXECDIR ?= $(ROOTPREFIX)/lib +LIBEXECDIR ?= $(PREFIX)/lib +SBINDIR ?= $(PREFIX)/sbin +DATADIR ?= $(PREFIX)/share +DOCDIR ?= $(DATADIR)/doc +MANDIR ?= $(DATADIR)/man +INCLUDEDIR ?= $(PREFIX)/include + +PYCODE = netplan/ $(wildcard src/*.py) $(wildcard tests/*.py) $(wildcard tests/generator/*.py) $(wildcard tests/dbus/*.py) + +# Order: Fedora/Mageia/openSUSE || Debian/Ubuntu || null +PYFLAKES3 ?= $(shell which pyflakes-3 || which pyflakes3 || echo true) +PYCODESTYLE3 ?= $(shell which pycodestyle-3 || which pycodestyle || which pep8 || echo true) +NOSETESTS3 ?= $(shell which nosetests-3 || which nosetests3 || echo true) + +default: netplan/_features.py generate netplan-dbus dbus/io.netplan.Netplan.service doc/netplan.html doc/netplan.5 doc/netplan-generate.8 doc/netplan-apply.8 doc/netplan-try.8 doc/netplan-dbus.8 doc/netplan-get.8 doc/netplan-set.8 + +%.o: src/%.c + $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -c $^ `pkg-config --cflags --libs glib-2.0 gio-2.0 yaml-0.1 uuid` + +libnetplan.so.$(NETPLAN_SOVER): parse.o netplan.o util.o validation.o error.o parse-nm.o + $(CC) -shared -Wl,-soname,libnetplan.so.$(NETPLAN_SOVER) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ `pkg-config --libs glib-2.0 gio-2.0 yaml-0.1` + ln -snf libnetplan.so.$(NETPLAN_SOVER) libnetplan.so + +generate: libnetplan.so.$(NETPLAN_SOVER) nm.o networkd.o openvswitch.o generate.o sriov.o + $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ -L. -lnetplan `pkg-config --cflags --libs glib-2.0 gio-2.0 yaml-0.1 uuid` + +netplan-dbus: src/dbus.c src/_features.h parse.o util.o validation.o error.o + $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $(patsubst %.h,,$^) `pkg-config --cflags --libs libsystemd glib-2.0 gio-2.0 yaml-0.1` + +src/_features.h: src/[^_]*.[hc] + printf "#include \nstatic const char *feature_flags[] __attribute__((__unused__)) = {\n" > $@ + awk 'match ($$0, /netplan-feature:.*/ ) { $$0=substr($$0, RSTART, RLENGTH); print "\""$$2"\"," }' $^ >> $@ + echo "NULL, };" >> $@ + +netplan/_features.py: src/[^_]*.[hc] + echo "# Generated file" > $@ + echo "NETPLAN_FEATURE_FLAGS = [" >> $@ + awk 'match ($$0, /netplan-feature:.*/ ) { $$0=substr($$0, RSTART, RLENGTH); print " \""$$2"\"," }' $^ >> $@ + echo "]" >> $@ + +clean: + rm -f netplan/_features.py src/_features.h + rm -f generate doc/*.html doc/*.[1-9] + rm -f *.o *.so* + rm -f netplan-dbus dbus/*.service + rm -f *.gcda *.gcno generate.info + rm -rf test-coverage .coverage coverage.xml + find . | grep -E "(__pycache__|\.pyc)" | xargs rm -rf + +check: default linting + tests/cli.py + LD_LIBRARY_PATH=. $(NOSETESTS3) -v --with-coverage + tests/validate_docs.sh + +linting: + $(PYFLAKES3) $(PYCODE) + $(PYCODESTYLE3) --max-line-length=130 $(PYCODE) + +coverage: | pre-coverage c-coverage python-coverage + +pre-coverage: + rm -f .coverage + $(MAKE) CFLAGS="-g -O0 --coverage" clean check + mkdir -p test-coverage/C test-coverage/python + +check-coverage: coverage + @if grep headerCovTableEntryHi test-coverage/C/index.html | grep -qv '100.*%'; then \ + echo "FAIL: Test coverage not 100%!" >&2; exit 1; \ + fi + python3-coverage report --omit=/usr* --show-missing --fail-under=100 + +c-coverage: + lcov --directory . --capture --gcov-tool=$(GCOV) -o generate.info + lcov --remove generate.info "/usr*" -o generate.info + genhtml -o test-coverage/C/ -t "generate test coverage" generate.info + +python-coverage: + python3-coverage html -d test-coverage/python --omit=/usr* || true + python3-coverage xml --omit=/usr* || true + +install: default + mkdir -p $(DESTDIR)/$(SBINDIR) $(DESTDIR)/$(ROOTLIBEXECDIR)/netplan $(DESTDIR)/$(SYSTEMD_GENERATOR_DIR) $(DESTDIR)/$(LIBDIR) + mkdir -p $(DESTDIR)/$(MANDIR)/man5 $(DESTDIR)/$(MANDIR)/man8 + mkdir -p $(DESTDIR)/$(DOCDIR)/netplan/examples + mkdir -p $(DESTDIR)/$(DATADIR)/netplan/netplan + mkdir -p $(DESTDIR)/$(INCLUDEDIR)/netplan + install -m 755 generate $(DESTDIR)/$(ROOTLIBEXECDIR)/netplan/ + find netplan/ -name '*.py' -exec install -Dm 644 "{}" "$(DESTDIR)/$(DATADIR)/netplan/{}" \; + install -m 755 src/netplan.script $(DESTDIR)/$(DATADIR)/netplan/ + ln -srf $(DESTDIR)/$(DATADIR)/netplan/netplan.script $(DESTDIR)/$(SBINDIR)/netplan + ln -srf $(DESTDIR)/$(ROOTLIBEXECDIR)/netplan/generate $(DESTDIR)/$(SYSTEMD_GENERATOR_DIR)/netplan + # lib + install -m 644 *.so.* $(DESTDIR)/$(LIBDIR)/ + ln -snf libnetplan.so.$(NETPLAN_SOVER) $(DESTDIR)/$(LIBDIR)/libnetplan.so + # headers, dev data + install -m 644 src/*.h $(DESTDIR)/$(INCLUDEDIR)/netplan/ + # TODO: install pkg-config once available + # docs, data + install -m 644 doc/*.html $(DESTDIR)/$(DOCDIR)/netplan/ + install -m 644 examples/*.yaml $(DESTDIR)/$(DOCDIR)/netplan/examples/ + install -m 644 doc/*.5 $(DESTDIR)/$(MANDIR)/man5/ + install -m 644 doc/*.8 $(DESTDIR)/$(MANDIR)/man8/ + install -T -D -m 644 netplan.completions $(DESTDIR)/$(BASH_COMPLETIONS_DIR)/netplan + # dbus + mkdir -p $(DESTDIR)/$(DATADIR)/dbus-1/system.d $(DESTDIR)/$(DATADIR)/dbus-1/system-services + install -m 755 netplan-dbus $(DESTDIR)/$(ROOTLIBEXECDIR)/netplan/ + install -m 644 dbus/io.netplan.Netplan.conf $(DESTDIR)/$(DATADIR)/dbus-1/system.d/ + install -m 644 dbus/io.netplan.Netplan.service $(DESTDIR)/$(DATADIR)/dbus-1/system-services/ + +%.service: %.service.in + sed -e "s#@ROOTLIBEXECDIR@#$(ROOTLIBEXECDIR)#" $< > $@ + + +%.html: %.md + pandoc -s --toc -o $@ $< + +doc/netplan.5: doc/manpage-header.md doc/netplan.md doc/manpage-footer.md + pandoc -s -o $@ $^ + +%.8: %.md + pandoc -s -o $@ $^ + +.PHONY: clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..31d422d --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# netplan - Backend-agnostic network configuration in YAML + +[![Build](https://github.com/canonical/netplan/workflows/Build/badge.svg?branch=master)](https://github.com/canonical/netplan/actions?query=branch%3Amaster+workflow%3ABuild) +[![Codecov](https://codecov.io/gh/canonical/netplan/branch/master/graph/badge.svg)](https://codecov.io/gh/canonical/netplan) + + +# Website + +http://netplan.io + +# Documentation + +An overview of the architecture can be found at [netplan.io/design](https://netplan.io/design) + +The full documentation for netplan is available in the [doc/netplan.md file](../master/doc/netplan.md) + +# Bug reports + +Please file bug reports in [Launchpad](https://bugs.launchpad.net/netplan/+filebug). + +# Contact us + +Please join us on IRC in #netplan at [Libera.Chat](https://libera.chat/). + +Our mailing list is [here](https://lists.launchpad.net/netplan-developers/). + +Email the list at [netplan-developers@lists.launchpad.net](mailto:netplan-developers@lists.launchpad.net). + diff --git a/TODO b/TODO new file mode 100644 index 0000000..610b28a --- /dev/null +++ b/TODO @@ -0,0 +1,41 @@ +- improve IPv6 RA handling + +- support tunnel device types + +- support ethtool/sysctl knobs (TSO, LRO, txqueuelen) + +- inspecting current network config via "netplan show $interface" for a + collated view of each interface's yaml. + +- debugging config generation via "netplan diff [backend|system]": + - netplan diff system: compare generated config with current ip addr output + - netplan diff backend: compare generated config with current config for backend + +- support other devices types from networkd/NetworkManager: + - infiniband + - veth + +- better handle VLAN Q-in-Q (mostly generation tweaks + patching backends) + +- support device aliases (eth0 + eth0.1; add eth0 to multiple bridges) + - workaround for two bridges is to use eth0 and vlan1 + +- make errors translatable + +- "netplan save" to capture kernel state into netplan YAML. + +- better parsing/validation for time-based values (ie. bond, bridge params) + +- openvswitch integration + +- wpa enterprise support + +- better parsing/validation for all schema + +- improve exit codes / behavior on error + +- integrate 'netplan try' in tmux/screen + +- replace nose for python tests with something else, preserving coverage reports + +- add automated integration tests for WPA Enterprise / 802.1x that can run self-contained diff --git a/dbus/io.netplan.Netplan.conf b/dbus/io.netplan.Netplan.conf new file mode 100644 index 0000000..c607d35 --- /dev/null +++ b/dbus/io.netplan.Netplan.conf @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/dbus/io.netplan.Netplan.service.in b/dbus/io.netplan.Netplan.service.in new file mode 100644 index 0000000..dafd487 --- /dev/null +++ b/dbus/io.netplan.Netplan.service.in @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=io.netplan.Netplan +Exec=@ROOTLIBEXECDIR@/netplan/netplan-dbus +User=root +AssumedAppArmorLabel=unconfined diff --git a/doc/example-config b/doc/example-config new file mode 100644 index 0000000..13ab91b --- /dev/null +++ b/doc/example-config @@ -0,0 +1,61 @@ +network: + version: 2 + # if specified, can only realistically have that value, as networkd cannot + # render wifi/3G. This would be shipped as a separate snippet by desktop images. + #renderer: NetworkManager + ethernets: + # opaque ID for physical interfaces, only referred to by other stanzas + id0: + match: + macaddress: 00:11:22:33:44:55 + wakeonlan: true + dhcp4: true + addresses: + - 192.168.14.2/24 + - "2001:1::1/64" + routes: + - to: default + via: 192.168.14.1 + - to: default + via: "2001:1::2" + - to: 11.22.0.0/16 + via: 192.168.14.3 + metric: 100 + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + lom: + match: + driver: ixgbe + # you are responsible for setting tight enough match rules + # that only match one device if you use set-name + set-name: lom1 + dhcp6: true + switchports: + # all cards on second PCI bus; unconfigured by themselves, will be added + # to br0 below (note: globbing is not supported by NetworkManager) + match: + name: enp2* + mtu: 1280 + wifis: + all-wlans: + # useful on a system where you know there is only ever going to be one device + match: {} + access-points: + "Joe's home": + # mode defaults to "managed" (client) + password: "s3kr1t" + # this creates an AP on wlp1s0 using hostapd; no match rules, thus ID is + # the interface name + wlp1s0: + access-points: + "guest": + mode: ap + # no WPA config implies default of open + bridges: + # the key name is the name for virtual (created) interfaces; no match: and + # set-name: allowed + br0: + # IDs of the components; switchports expands into multiple interfaces + interfaces: [wlp1s0, switchports] + dhcp4: true diff --git a/doc/manpage-footer.md b/doc/manpage-footer.md new file mode 100644 index 0000000..ec1dda7 --- /dev/null +++ b/doc/manpage-footer.md @@ -0,0 +1,3 @@ +# SEE ALSO + + **netplan-generate**(8), **netplan-apply**(8), **netplan-try**(8), **netplan-get**(8), **netplan-set**(8), **netplan-dbus**(8), **systemd-networkd**(8), **NetworkManager**(8) diff --git a/doc/manpage-header.md b/doc/manpage-header.md new file mode 100644 index 0000000..0f5231e --- /dev/null +++ b/doc/manpage-header.md @@ -0,0 +1,21 @@ +--- +title: netplan +section: 5 +author: +- Mathieu Trudel-Lapierre () +- Martin Pitt () +... + +# NAME + +netplan - YAML network configuration abstraction for various backends + +# SYNOPSIS + +**netplan** [ *COMMAND* | help ] + +# COMMANDS + +See **netplan help** for a list of available commands on this system. + +# DESCRIPTION diff --git a/doc/netplan-apply.md b/doc/netplan-apply.md new file mode 100644 index 0000000..153acb1 --- /dev/null +++ b/doc/netplan-apply.md @@ -0,0 +1,62 @@ +--- +title: netplan-apply +section: 8 +author: +- Daniel Axtens () +... + +# NAME + +netplan-apply - apply configuration from netplan YAML files to a running system + +# SYNOPSIS + + **netplan** [--debug] **apply** -h | --help + + **netplan** [--debug] **apply** + +# DESCRIPTION + +**netplan apply** applies the current netplan configuration to a running system. + +The process works as follows: + + 1. The backend configuration is generated from netplan YAML files. + + 2. The appropriate backends (**systemd-networkd**(8) or + **NetworkManager**(8)) are invoked to bring up configured interfaces. + + 3. **netplan apply** iterates through interfaces that are still down, unbinding + them from their drivers, and rebinding them. This gives **udev**(7) renaming + rules the opportunity to run. + + 4. If any devices have been rebound, the appropriate backends are re-invoked in + case more matches can be done. + +For information about the generation step, see +**netplan-generate**(8). For details of the configuration file format, +see **netplan**(5). + +# OPTIONS + + -h, --help +: Print basic help. + + --debug +: Print debugging output during the process. + +# KNOWN ISSUES + +**netplan apply** will not remove virtual devices such as bridges +and bonds that have been created, even if they are no longer described +in the netplan configuration. + +This can be resolved by manually removing the virtual device (for +example ``ip link delete dev bond0``) and then running **netplan +apply**, or by rebooting. + + +# SEE ALSO + + **netplan**(5), **netplan-generate**(8), **netplan-try**(8), **udev**(7), + **systemd-networkd.service**(8), **NetworkManager**(8) diff --git a/doc/netplan-dbus.md b/doc/netplan-dbus.md new file mode 100644 index 0000000..2193a73 --- /dev/null +++ b/doc/netplan-dbus.md @@ -0,0 +1,43 @@ +--- +title: netplan-dbus +section: 8 +author: +- Lukas Märdian () +... + +# NAME + +netplan-dbus - daemon to access netplan's functionality via a DBus API + +# SYNOPSIS + + **netplan-dbus** + +# DESCRIPTION + +**netplan-dbus** is a DBus daemon, providing ``io.netplan.Netplan`` on the system bus. The ``/io/netplan/Netplan`` object provides an ``io.netplan.Netplan`` interface, offering the following methods: + + * ``Apply() -> b``: calls **netplan apply** and returns a success or failure status. + * ``Generate() -> b``: calls **netplan generate** and returns a success or failure status. + * ``Info() -> a(sv)``: returns a dict "Features -> as", containing an array of all available feature flags. + * ``Config() -> o``: prepares a new config object as ``/io/netplan/Netplan/config/``, by copying the current state from ``/{etc,run,lib}/netplan/*.yaml`` + +The ``/io/netplan/Netplan/config/`` objects provide a ``io.netplan.Netplan.Config`` interface, offering the following methods: + + * ``Get() -> s``: calls **netplan get --root-dir=/tmp/netplan-config-ID all** and returns the merged YAML config of the the given config object's state + * ``Set(s:CONFIG_DELTA, s:ORIGIN_HINT) -> b``: calls **netplan set --root-dir=/tmp/netplan-config-ID --origin-hint=ORIGIN_HINT CONFIG_DELTA** + + CONFIG_DELTA can be something like: ``network.ethernets.eth0.dhcp4=true`` and ORIGIN_HINT can be something like: ``70-snapd`` (it will then write the config to ``70-snapd.yaml``). Once ``Set()`` is called on a config object, all other current and future config objects are being invalidated and cannot ``Set()`` or ``Try()/Apply()`` anymore, due to this pending dirty state. After the dirty config object is rejected via ``Cancel()``, the other config objects are valid again. If the dirty config object is accepted via ``Apply()``, newly created config objects will be valid, while the older states will stay invalid. + + * ``Try(u:TIMEOUT_SEC) -> b``: replaces the main netplan configuration with this config object's state and calls **netplan try --timeout=TIMEOUT_SEC** + * ``Cancel() -> b``: rejects a currently running ``Try()`` attempt on this config object and/or discards the config object + * ``Apply() -> b``: replaces the main netplan configuration with this config object's state and calls **netplan apply** + +For information about the Apply()/Try()/Get()/Set() functionality, see +**netplan-apply**(8)/**netplan-try**(8)/**netplan-get**(8)/**netplan-set**(8) +accordingly. For details of the configuration file format, see **netplan**(5). + +# SEE ALSO + + **netplan**(5), **netplan-apply**(8), **netplan-try**(8), **netplan-get**(8), + **netplan-set**(8) diff --git a/doc/netplan-generate.md b/doc/netplan-generate.md new file mode 100644 index 0000000..5ba5ee1 --- /dev/null +++ b/doc/netplan-generate.md @@ -0,0 +1,88 @@ +--- +title: netplan-generate +section: 8 +author: +- Daniel Axtens () +... + +# NAME + +netplan-generate - generate backend configuration from netplan YAML files + +# SYNOPSIS + + **netplan** [--debug] **generate** -h | --help + + **netplan** [--debug] **generate** [--root-dir _ROOT_DIR_] [--mapping _MAPPING_] + +# DESCRIPTION + +netplan generate converts netplan YAML into configuration files +understood by the backends (**systemd-networkd**(8) or +**NetworkManager**(8)). It *does not* apply the generated +configuration. + +You will not normally need to run this directly as it is run by +**netplan apply**, **netplan try**, or at boot. + +Only if executed during the systemd ``initializing`` phase +(i.e. "Early bootup, before ``basic.target`` is reached"), will +it attempt to start/apply the newly created service units. +**Requires feature: generate-just-in-time** + +For details of the configuration file format, see **netplan**(5). + +# OPTIONS + + -h, --help +: Print basic help. + + --debug +: Print debugging output during the process. + + --root-dir _ROOT_DIR_ +: Instead of looking in /{lib,etc,run}/netplan, look in + /_ROOT_DIR_/{lib,etc,run}/netplan + + --mapping _MAPPING_ +: Instead of generating output files, parse the configuration files + and print some internal information about the device specified in + _MAPPING_. + +# HANDLING MULTIPLE FILES + +There are 3 locations that netplan generate considers: + + * /lib/netplan/*.yaml + * /etc/netplan/*.yaml + * /run/netplan/*.yaml + +If there are multiple files with exactly the same name, then only one +will be read. A file in /run/netplan will shadow - completely replace +- a file with the same name in /etc/netplan. A file in /etc/netplan +will itself shadow a file in /lib/netplan. + +Or in other words, /run/netplan is top priority, then /etc/netplan, +with /lib/netplan having the lowest priority. + +If there are files with different names, then they are considered in +lexicographical order - regardless of the directory they are in. Later +files add to or override earlier files. For example, +/run/netplan/10-foo.yaml would be updated by /lib/netplan/20-abc.yaml. + +If you have two files with the same key/setting, the following rules +apply: + + * If the values are YAML boolean or scalar values (numbers and + strings) the old value is overwritten by the new value. + + * If the values are sequences, the sequences are concatenated - the + new values are appended to the old list. + + * If the values are mappings, netplan will examine the elements + of the mappings in turn using these rules. + +# SEE ALSO + + **netplan**(5), **netplan-apply**(8), **netplan-try**(8), + **systemd-networkd**(8), **NetworkManager**(8) diff --git a/doc/netplan-get.md b/doc/netplan-get.md new file mode 100644 index 0000000..5c9b978 --- /dev/null +++ b/doc/netplan-get.md @@ -0,0 +1,39 @@ +--- +title: netplan-get +section: 8 +author: +- Lukas Märdian (lukas.maerdian@canonical.com) +... + +# NAME + +netplan-get - read merged netplan YAML configuration + +# SYNOPSIS + + **netplan** [--debug] **get** -h | --help + + **netplan** [--debug] **get** [--root-dir=ROOT_DIR] [key] + +# DESCRIPTION + +**netplan get [key]** reads all YAML files from ``/{etc,lib,run}/netplan/*.yaml`` and returns a merged view of the current configuration + +You can specify ``all`` as a key (the default) to get the full YAML tree or extract a subtree by specifying a nested key like: ``[network.]ethernets.eth0``. + +For details of the configuration file format, see **netplan**(5). + +# OPTIONS + + -h, --help +: Print basic help. + + --debug +: Print debugging output during the process. + + --root-dir +: Read YAML files from this root instead of / + +# SEE ALSO + + **netplan**(5), **netplan-set**(8), **netplan-dbus**(8) diff --git a/doc/netplan-set.md b/doc/netplan-set.md new file mode 100644 index 0000000..f2c912d --- /dev/null +++ b/doc/netplan-set.md @@ -0,0 +1,42 @@ +--- +title: netplan-set +section: 8 +author: +- Lukas Märdian (lukas.maerdian@canonical.com) +... + +# NAME + +netplan-set - write netplan YAML configuration snippets to file + +# SYNOPSIS + + **netplan** [--debug] **set** -h | --help + + **netplan** [--debug] **set** [--root-dir=ROOT_DIR] [--origin-hint=ORIGIN_HINT] [key=value] + +# DESCRIPTION + +**netplan set [key=value]** writes a given key/value pair or YAML subtree into a YAML file in ``/etc/netplan/`` and validates its format. + +You can specify a single value as: ``"[network.]ethernets.eth0.dhcp4=[1.2.3.4/24, 5.6.7.8/24]"`` or a full subtree as: ``"[network.]ethernets.eth0={dhcp4: true, dhcp6: true}"``. + +For details of the configuration file format, see **netplan**(5). + +# OPTIONS + + -h, --help +: Print basic help. + + --debug +: Print debugging output during the process. + + --root-dir +: Write YAML files into this root instead of / + + --origin-hint +: Specify a name for the config file, e.g.: ``70-netplan-set`` => ``/etc/netplan/70-netplan-set.yaml`` + +# SEE ALSO + + **netplan**(5), **netplan-get**(8), **netplan-dbus**(8) diff --git a/doc/netplan-try.md b/doc/netplan-try.md new file mode 100644 index 0000000..0167913 --- /dev/null +++ b/doc/netplan-try.md @@ -0,0 +1,64 @@ +--- +title: netplan-try +section: 8 +author: +- Daniel Axtens () +... + +# NAME + +netplan-try - try a configuration, optionally rolling it back + +# SYNOPSIS + + **netplan** [--debug] **try** -h | --help + + **netplan** [--debug] **try** [--config-file _CONFIG_FILE_] [--timeout _TIMEOUT_] + +# DESCRIPTION + +**netplan try** takes a **netplan**(5) configuration, applies it, and +automatically rolls it back if the user does not confirm the +configuration within a time limit. + +A configuration can be confirmed or rejected interactively or by sending the +SIGUSR1 or SIGINT signals. + +This may be especially useful on remote systems, to prevent an +administrator being permanently locked out of systems in the case of a +network configuration error. + +# OPTIONS + + -h, --help +: Print basic help. + + --debug +: Print debugging output during the process. + + --config-file _CONFIG_FILE_ +: In addition to the usual configuration, apply _CONFIG_FILE_. It must + be a YAML file in the **netplan**(5) format. + + --timeout _TIMEOUT_ +: Wait for _TIMEOUT_ seconds before reverting. Defaults to 120 + seconds. Note that some network configurations (such as STP) may take + over a minute to settle. + +# KNOWN ISSUES + +**netplan try** uses similar procedures to **netplan apply**, so some +of the same caveats apply around virtual devices. + +There are also some known bugs: if **netplan try** times out or is +cancelled, make sure to verify if the network configuration has in +fact been reverted. + +As with **netplan apply**, a reboot should fix any issues. However, be +sure to verify that the config on disk is in the state you expect +before rebooting! + +# SEE ALSO + + **netplan**(5), **netplan-generate**(8), **netplan-apply**(8) + diff --git a/doc/netplan.md b/doc/netplan.md new file mode 100644 index 0000000..1050dff --- /dev/null +++ b/doc/netplan.md @@ -0,0 +1,1455 @@ +## Introduction +Distribution installers, cloud instantiation, image builds for particular +devices, or any other way to deploy an operating system put its desired +network configuration into YAML configuration file(s). During +early boot, the netplan "network renderer" runs which reads +``/{lib,etc,run}/netplan/*.yaml`` and writes configuration to ``/run`` to hand +off control of devices to the specified networking daemon. + + - Configured devices get handled by systemd-networkd by default, + unless explicitly marked as managed by a specific renderer (NetworkManager) + - Devices not covered by the network config do not get touched at all. + - Usable in initramfs (few dependencies and fast) + - No persistent generated config, only original YAML config + - Parser supports multiple config files to allow applications like libvirt or lxd + to package up expected network config (``virbr0``, ``lxdbr0``), or to change the + global default policy to use NetworkManager for everything. + - Retains the flexibility to change backends/policy later or adjust to + removing NetworkManager, as generated configuration is ephemeral. + +## General structure +netplan's configuration files use the +[YAML]() format. All +``/{lib,etc,run}/netplan/*.yaml`` are considered. Lexicographically later files +(regardless of in which directory they are) amend (new mapping keys) or +override (same mapping keys) previous ones. A file in ``/run/netplan`` +completely shadows a file with same name in ``/etc/netplan``, and a file in +either of those directories shadows a file with the same name in +``/lib/netplan``. + +The top-level node in a netplan configuration file is a ``network:`` mapping +that contains ``version: 2`` (the YAML currently being used by curtin, MaaS, +etc. is version 1), and then device definitions grouped by their type, such as +``ethernets:``, ``modems:``, ``wifis:``, or ``bridges:``. These are the types that our +renderer can understand and are supported by our backends. + +Each type block contains device definitions as a map where the keys (called +"configuration IDs") are defined as below. + +## Device configuration IDs +The key names below the per-device-type definition maps (like ``ethernets:``) +are called "ID"s. They must be unique throughout the entire set of +configuration files. Their primary purpose is to serve as anchor names for +composite devices, for example to enumerate the members of a bridge that is +currently being defined. + +(Since 0.97) If an interface is defined with an ID in a configuration file; it will +be brought up by the applicable renderer. To not have netplan touch an interface +at all, it should be completely omitted from the netplan configuration files. + +There are two physically/structurally different classes of device definitions, +and the ID field has a different interpretation for each: + +Physical devices + +: (Examples: ethernet, modem, wifi) These can dynamically come and go between + reboots and even during runtime (hotplugging). In the generic case, they + can be selected by ``match:`` rules on desired properties, such as name/name + pattern, MAC address, driver, or device paths. In general these will match + any number of devices (unless they refer to properties which are unique + such as the full path or MAC address), so without further knowledge about + the hardware these will always be considered as a group. + + It is valid to specify no match rules at all, in which case the ID field is + simply the interface name to be matched. This is mostly useful if you want + to keep simple cases simple, and it's how network device configuration has + been done for a long time. + + If there are ``match``: rules, then the ID field is a purely opaque name + which is only being used for references from definitions of compound + devices in the config. + + +Virtual devices + +: (Examples: veth, bridge, bond) These are fully under the control of the + config file(s) and the network stack. I. e. these devices are being created + instead of matched. Thus ``match:`` and ``set-name:`` are not applicable for + these, and the ID field is the name of the created virtual device. + +## Common properties for physical device types + +``match`` (mapping) + +: This selects a subset of available physical devices by various hardware + properties. The following configuration will then apply to all matching + devices, as soon as they appear. *All* specified properties must match. + + ``name`` (scalar) + : Current interface name. Globs are supported, and the primary use case + for matching on names, as selecting one fixed name can be more easily + achieved with having no ``match:`` at all and just using the ID (see + above). + (``NetworkManager``: as of v1.14.0) + + ``macaddress`` (scalar) + : Device's MAC address in the form "XX:XX:XX:XX:XX:XX". Globs are not + allowed. + + ``driver`` (scalar) + : Kernel driver name, corresponding to the ``DRIVER`` udev property. + Globs are supported. Matching on driver is *only* supported with + networkd. + + Examples: + + - all cards on second PCI bus: + + match: + name: enp2* + + - fixed MAC address: + + match: + macaddress: 11:22:33:AA:BB:FF + + - first card of driver ``ixgbe``: + + match: + driver: ixgbe + name: en*s0 + +``set-name`` (scalar) + +: When matching on unique properties such as path or MAC, or with additional + assumptions such as "there will only ever be one wifi device", + match rules can be written so that they only match one device. Then this + property can be used to give that device a more specific/desirable/nicer + name than the default from udev’s ifnames. Any additional device that + satisfies the match rules will then fail to get renamed and keep the + original kernel name (and dmesg will show an error). + +``wakeonlan`` (bool) + +: Enable wake on LAN. Off by default. + + **Note:** This will not work reliably for devices matched by name + only and rendered by networkd, due to interactions with device + renaming in udev. Match devices by MAC when setting wake on LAN. + +``emit-lldp`` (bool) – since **0.99** + +: (networkd backend only) Whether to emit LLDP packets. Off by default. + +``openvswitch`` (mapping) – since **0.100** + +: This provides additional configuration for the network device for openvswitch. + If openvswitch is not available on the system, netplan treats the presence of + openvswitch configuration as an error. + + Any supported network device that is declared with the ``openvswitch`` mapping + (or any bond/bridge that includes an interface with an openvswitch configuration) + will be created in openvswitch instead of the defined renderer. + In the case of a ``vlan`` definition declared the same way, netplan will create + a fake VLAN bridge in openvswitch with the requested vlan properties. + + ``external-ids`` (mapping) – since **0.100** + : Passed-through directly to OpenVSwitch + + ``other-config`` (mapping) – since **0.100** + : Passed-through directly to OpenVSwitch + + ``lacp`` (scalar) – since **0.100** + : Valid for bond interfaces. Accepts ``active``, ``passive`` or ``off`` (the default). + + ``fail-mode`` (scalar) – since **0.100** + : Valid for bridge interfaces. Accepts ``secure`` or ``standalone`` (the default). + + ``mcast-snooping`` (bool) – since **0.100** + : Valid for bridge interfaces. False by default. + + ``protocols`` (sequence of scalars) – since **0.100** + : Valid for bridge interfaces or the network section. List of protocols to be used when + negotiating a connection with the controller. Accepts ``OpenFlow10``, ``OpenFlow11``, + ``OpenFlow12``, ``OpenFlow13``, ``OpenFlow14``, ``OpenFlow15`` and ``OpenFlow16``. + + ``rstp`` (bool) – since **0.100** + : Valid for bridge interfaces. False by default. + + ``controller`` (mapping) – since **0.100** + : Valid for bridge interfaces. Specify an external OpenFlow controller. + + ``addresses`` (sequence of scalars) + : Set the list of addresses to use for the controller targets. The + syntax of these addresses is as defined in ovs-vsctl(8). Example: + addresses: ``[tcp:127.0.0.1:6653, "ssl:[fe80::1234%eth0]:6653"]`` + + ``connection-mode`` (scalar) + : Set the connection mode for the controller. Supported options are + ``in-band`` and ``out-of-band``. The default is ``in-band``. + + ``ports`` (sequence of sequence of scalars) – since **0.100** + : OpenvSwitch patch ports. Each port is declared as a pair of names + which can be referenced as interfaces in dependent virtual devices + (bonds, bridges). + + Example: + + openvswitch: + ports: + - [patch0-1, patch1-0] + + ``ssl`` (mapping) – since **0.100** + : Valid for global ``openvswitch`` settings. Options for configuring SSL + server endpoint for the switch. + + ``ca-cert`` (scalar) + : Path to a file containing the CA certificate to be used. + + ``certificate`` (scalar) + : Path to a file containing the server certificate. + + ``private-key`` (scalar) + : Path to a file containing the private key for the server. + +## Common properties for all device types + +``renderer`` (scalar) + +: Use the given networking backend for this definition. Currently supported are + ``networkd`` and ``NetworkManager``. This property can be specified globally + in ``network:``, for a device type (in e. g. ``ethernets:``) or + for a particular device definition. Default is ``networkd``. + + (Since 0.99) The ``renderer`` property has one additional acceptable value for vlan + objects (i. e. defined in ``vlans:``): ``sriov``. If a vlan is defined with the + ``sriov`` renderer for an SR-IOV Virtual Function interface, this causes netplan to + set up a hardware VLAN filter for it. There can be only one defined per VF. + +``dhcp4`` (bool) + +: Enable DHCP for IPv4. Off by default. + +``dhcp6`` (bool) + +: Enable DHCP for IPv6. Off by default. This covers both stateless DHCP - + where the DHCP server supplies information like DNS nameservers but not the + IP address - and stateful DHCP, where the server provides both the address + and the other information. + + If you are in an IPv6-only environment with completely stateless + autoconfiguration (SLAAC with RDNSS), this option can be set to cause the + interface to be brought up. (Setting accept-ra alone is not sufficient.) + Autoconfiguration will still honour the contents of the router advertisement + and only use DHCP if requested in the RA. + + Note that **``rdnssd``**(8) is required to use RDNSS with networkd. No extra + software is required for NetworkManager. + +``ipv6-mtu`` (scalar) – since **0.98** +: Set the IPv6 MTU (only supported with `networkd` backend). Note + that needing to set this is an unusual requirement. + + **Requires feature: ipv6-mtu** + +``ipv6-privacy`` (bool) + +: Enable IPv6 Privacy Extensions (RFC 4941) for the specified interface, and + prefer temporary addresses. Defaults to false - no privacy extensions. There + is currently no way to have a private address but prefer the public address. + +``link-local`` (sequence of scalars) + +: Configure the link-local addresses to bring up. Valid options are 'ipv4' + and 'ipv6', which respectively allow enabling IPv4 and IPv6 link local + addressing. If this field is not defined, the default is to enable only + IPv6 link-local addresses. If the field is defined but configured as an + empty set, IPv6 link-local addresses are disabled as well as IPv4 link- + local addresses. + + This feature enables or disables link-local addresses for a protocol, but + the actual implementation differs per backend. On networkd, this directly + changes the behavior and may add an extra address on an interface. When + using the NetworkManager backend, enabling link-local has no effect if the + interface also has DHCP enabled. + + Example to enable only IPv4 link-local: ``link-local: [ ipv4 ]`` + Example to enable all link-local addresses: ``link-local: [ ipv4, ipv6 ]`` + Example to disable all link-local addresses: ``link-local: [ ]`` + +``critical`` (bool) + +: Designate the connection as "critical to the system", meaning that special + care will be taken by to not release the assigned IP when the daemon is + restarted. (not recognized by NetworkManager) + +``dhcp-identifier`` (scalar) + +: (networkd backend only) Sets the source of DHCPv4 client identifier. If ``mac`` + is specified, the MAC address of the link is used. If this option is omitted, + or if ``duid`` is specified, networkd will generate an RFC4361-compliant client + identifier for the interface by combining the link's IAID and DUID. + + ``dhcp4-overrides`` (mapping) + + : (networkd backend only) Overrides default DHCP behavior; see the + ``DHCP Overrides`` section below. + + ``dhcp6-overrides`` (mapping) + + : (networkd backend only) Overrides default DHCP behavior; see the + ``DHCP Overrides`` section below. + +``accept-ra`` (bool) + +: Accept Router Advertisement that would have the kernel configure IPv6 by itself. + When enabled, accept Router Advertisements. When disabled, do not respond to + Router Advertisements. If unset use the host kernel default setting. + +``addresses`` (sequence of scalars and mappings) + +: Add static addresses to the interface in addition to the ones received + through DHCP or RA. Each sequence entry is in CIDR notation, i. e. of the + form ``addr/prefixlen``. ``addr`` is an IPv4 or IPv6 address as recognized + by **``inet_pton``**(3) and ``prefixlen`` the number of bits of the subnet. + + For virtual devices (bridges, bonds, vlan) if there is no address + configured and DHCP is disabled, the interface may still be brought online, + but will not be addressable from the network. + + In addition to the addresses themselves one can specify configuration + parameters as mappings. Current supported options are: + + ``lifetime`` (scalar) – since **0.100** + : Default: ``forever``. This can be ``forever`` or ``0`` and corresponds + to the ``PreferredLifetime`` option in ``systemd-networkd``'s Address + section. Currently supported on the ``networkd`` backend only. + + ``label`` (scalar) – since **0.100** + : An IP address label, equivalent to the ``ip address label`` + command. Currently supported on the ``networkd`` backend only. + + Example: ``addresses: [192.168.14.2/24, "2001:1::1/64"]`` + + Example: + + ethernets: + eth0: + addresses: + - 10.0.0.15/24: + lifetime: 0 + label: "maas" + - "2001:1::1/64" + +``ipv6-address-generation`` (scalar) – since **0.99** + +: Configure method for creating the address for use with RFC4862 IPv6 + Stateless Address Autoconfiguration (only supported with `NetworkManager` + backend). Possible values are ``eui64`` or ``stable-privacy``. + +``ipv6-address-token`` (scalar) – since **0.100** + +: Define an IPv6 address token for creating a static interface identifier for + IPv6 Stateless Address Autoconfiguration. This is mutually exclusive with + ``ipv6-address-generation``. + +``gateway4``, ``gateway6`` (scalar) + +: Deprecated, see ``Default routes``. + Set default gateway for IPv4/6, for manual address configuration. This + requires setting ``addresses`` too. Gateway IPs must be in a form + recognized by **``inet_pton``**(3). There should only be a single gateway + per IP address family set in your global config, to make it unambiguous. + If you need multiple default routes, please define them via + ``routing-policy``. + + Example for IPv4: ``gateway4: 172.16.0.1`` + Example for IPv6: ``gateway6: "2001:4::1"`` + +``nameservers`` (mapping) + +: Set DNS servers and search domains, for manual address configuration. There +are two supported fields: ``addresses:`` is a list of IPv4 or IPv6 addresses +similar to ``gateway*``, and ``search:`` is a list of search domains. + + Example: + + ethernets: + id0: + [...] + nameservers: + search: [lab, home] + addresses: [8.8.8.8, "FEDC::1"] + +``macaddress`` (scalar) + +: Set the device's MAC address. The MAC address must be in the form + "XX:XX:XX:XX:XX:XX". + + **Note:** This will not work reliably for devices matched by name + only and rendered by networkd, due to interactions with device + renaming in udev. Match devices by MAC when setting MAC addresses. + + Example: + + ethernets: + id0: + match: + macaddress: 52:54:00:6b:3c:58 + [...] + macaddress: 52:54:00:6b:3c:59 + +``mtu`` (scalar) + +: Set the Maximum Transmission Unit for the interface. The default is 1500. + Valid values depend on your network interface. + + **Note:** This will not work reliably for devices matched by name + only and rendered by networkd, due to interactions with device + renaming in udev. Match devices by MAC when setting MTU. + +``optional`` (bool) + +: An optional device is not required for booting. Normally, networkd will + wait some time for device to become configured before proceeding with + booting. However, if a device is marked as optional, networkd will not wait + for it. This is *only* supported by networkd, and the default is false. + + Example: + + ethernets: + eth7: + # this is plugged into a test network that is often + # down - don't wait for it to come up during boot. + dhcp4: true + optional: true + +``optional-addresses`` (sequence of scalars) + +: Specify types of addresses that are not required for a device to be + considered online. This changes the behavior of backends at boot time to + avoid waiting for addresses that are marked optional, and thus consider + the interface as "usable" sooner. This does not disable these addresses, + which will be brought up anyway. + + Example: + + ethernets: + eth7: + dhcp4: true + dhcp6: true + optional-addresses: [ ipv4-ll, dhcp6 ] + +``activation-mode`` (scalar) – since **0.103** + +: Allows specifying the management policy of the selected interface. By + default, netplan brings up any configured interface if possible. Using the + ``activation-mode`` setting users can override that behavior by either + specifying ``manual``, to hand over control over the interface state to the + administrator or (for networkd backend *only*) ``off`` to force the link + in a down state at all times. Any interface with ``activation-mode`` + defined is implicitly considered ``optional``. + Supported officially as of ``networkd`` v248+. + + Example: + + ethernets: + eth1: + # this interface will not be put into an UP state automatically + dhcp4: true + activation-mode: manual + +``routes`` (sequence of mappings) + +: Configure static routing for the device; see the ``Routing`` section below. + +``routing-policy`` (sequence of mappings) + +: Configure policy routing for the device; see the ``Routing`` section below. + +## DHCP Overrides +Several DHCP behavior overrides are available. Most currently only have any +effect when using the ``networkd`` backend, with the exception of ``use-routes`` +and ``route-metric``. + +Overrides only have an effect if the corresponding ``dhcp4`` or ``dhcp6`` is +set to ``true``. + +If both ``dhcp4`` and ``dhcp6`` are ``true``, the ``networkd`` backend requires +that ``dhcp4-overrides`` and ``dhcp6-overrides`` contain the same keys and +values. If the values do not match, an error will be shown and the network +configuration will not be applied. + +When using the NetworkManager backend, different values may be specified for +``dhcp4-overrides`` and ``dhcp6-overrides``, and will be applied to the DHCP +client processes as specified in the netplan YAML. + +``dhcp4-overrides``, ``dhcp6-overrides`` (mapping) + +: The ``dhcp4-overrides`` and ``dhcp6-overrides`` mappings override the + default DHCP behavior. + + ``use-dns`` (bool) + : Default: ``true``. When ``true``, the DNS servers received from the + DHCP server will be used and take precedence over any statically + configured ones. Currently only has an effect on the ``networkd`` + backend. + + ``use-ntp`` (bool) + : Default: ``true``. When ``true``, the NTP servers received from the + DHCP server will be used by systemd-timesyncd and take precedence + over any statically configured ones. Currently only has an effect on + the ``networkd`` backend. + + ``send-hostname`` (bool) + : Default: ``true``. When ``true``, the machine's hostname will be sent + to the DHCP server. Currently only has an effect on the ``networkd`` + backend. + + ``use-hostname`` (bool) + : Default: ``true``. When ``true``, the hostname received from the DHCP + server will be set as the transient hostname of the system. Currently + only has an effect on the ``networkd`` backend. + + ``use-mtu`` (bool) + : Default: ``true``. When ``true``, the MTU received from the DHCP + server will be set as the MTU of the network interface. When ``false``, + the MTU advertised by the DHCP server will be ignored. Currently only + has an effect on the ``networkd`` backend. + + ``hostname`` (scalar) + : Use this value for the hostname which is sent to the DHCP server, + instead of machine's hostname. Currently only has an effect on the + ``networkd`` backend. + + ``use-routes`` (bool) + : Default: ``true``. When ``true``, the routes received from the DHCP + server will be installed in the routing table normally. When set to + ``false``, routes from the DHCP server will be ignored: in this case, + the user is responsible for adding static routes if necessary for + correct network operation. This allows users to avoid installing a + default gateway for interfaces configured via DHCP. Available for + both the ``networkd`` and ``NetworkManager`` backends. + + ``route-metric`` (scalar) + : Use this value for default metric for automatically-added routes. + Use this to prioritize routes for devices by setting a lower metric + on a preferred interface. Available for both the ``networkd`` and + ``NetworkManager`` backends. + + ``use-domains`` (scalar) – since **0.98** + : Takes a boolean, or the special value "route". When true, the domain + name received from the DHCP server will be used as DNS search domain + over this link, similar to the effect of the Domains= setting. If set + to "route", the domain name received from the DHCP server will be + used for routing DNS queries only, but not for searching, similar to + the effect of the Domains= setting when the argument is prefixed with + "~". + + **Requires feature: dhcp-use-domains** + + +## Routing +Complex routing is possible with netplan. Standard static routes as well +as policy routing using routing tables are supported via the ``networkd`` +backend. + +These options are available for all types of interfaces. + +### Default routes + +The most common need for routing concerns the definition of default routes to +reach the wider Internet. Those default routes can only defined once per IP family +and routing table. A typical example would look like the following: + +```yaml +eth0: + [...] + routes: + - to: default # could be 0/0 or 0.0.0.0/0 optionally + via: 10.0.0.1 + metric: 100 + on-link: true + - to: default # could be ::/0 optionally + via: cf02:de:ad:be:ef::2 +eth1: + [...] + routes: + - to: default + via: 172.134.67.1 + metric: 100 + on-link: true + table: 76 # Not on the main routing table, does not conflict with the eth0 default route +``` + +``routes`` (mapping) + +: The ``routes`` block defines standard static routes for an interface. + At least ``to`` and ``via`` must be specified. + + For ``from``, ``to``, and ``via``, both IPv4 and IPv6 addresses are + recognized, and must be in the form ``addr/prefixlen`` or ``addr``. + + ``from`` (scalar) + : Set a source IP address for traffic going through the route. + (``NetworkManager``: as of v1.8.0) + + ``to`` (scalar) + : Destination address for the route. + + ``via`` (scalar) + : Address to the gateway to use for this route. + + ``on-link`` (bool) + : When set to "true", specifies that the route is directly connected + to the interface. + (``NetworkManager``: as of v1.12.0 for IPv4 and v1.18.0 for IPv6) + + ``metric`` (scalar) + : The relative priority of the route. Must be a positive integer value. + + ``type`` (scalar) + : The type of route. Valid options are "unicast" (default), + "unreachable", "blackhole" or "prohibit". + + ``scope`` (scalar) + : The route scope, how wide-ranging it is to the network. Possible + values are "global", "link", or "host". ``NetworkManager`` does + not support setting a scope. + + ``table`` (scalar) + : The table number to use for the route. In some scenarios, it may be + useful to set routes in a separate routing table. It may also be used + to refer to routing policy rules which also accept a ``table`` + parameter. Allowed values are positive integers starting from 1. + Some values are already in use to refer to specific routing tables: + see ``/etc/iproute2/rt_tables``. + (``NetworkManager``: as of v1.10.0) + + ``mtu`` (scalar) – since **0.101** + : The MTU to be used for the route, in bytes. Must be a positive integer + value. + + ``congestion-window`` (scalar) – since **0.102** + : The congestion window to be used for the route, represented by number + of segments. Must be a positive integer value. + + ``advertised-receive-window`` (scalar) – since **0.102** + : The receive window to be advertised for the route, represented by + number of segments. Must be a positive integer value. + +``routing-policy`` (mapping) + +: The ``routing-policy`` block defines extra routing policy for a network, + where traffic may be handled specially based on the source IP, firewall + marking, etc. + + For ``from``, ``to``, both IPv4 and IPv6 addresses are recognized, and + must be in the form ``addr/prefixlen`` or ``addr``. + + ``from`` (scalar) + : Set a source IP address to match traffic for this policy rule. + + ``to`` (scalar) + : Match on traffic going to the specified destination. + + ``table`` (scalar) + : The table number to match for the route. In some scenarios, it may be + useful to set routes in a separate routing table. It may also be used + to refer to routes which also accept a ``table`` parameter. + Allowed values are positive integers starting from 1. + Some values are already in use to refer to specific routing tables: + see ``/etc/iproute2/rt_tables``. + + ``priority`` (scalar) + : Specify a priority for the routing policy rule, to influence the order + in which routing rules are processed. A higher number means lower + priority: rules are processed in order by increasing priority number. + + ``mark`` (scalar) + : Have this routing policy rule match on traffic that has been marked + by the iptables firewall with this value. Allowed values are positive + integers starting from 1. + + ``type-of-service`` (scalar) + : Match this policy rule based on the type of service number applied to + the traffic. + +## Authentication +Netplan supports advanced authentication settings for ethernet and wifi +interfaces, as well as individual wifi networks, by means of the ``auth`` block. + +``auth`` (mapping) + +: Specifies authentication settings for a device of type ``ethernets:``, or + an ``access-points:`` entry on a ``wifis:`` device. + + The ``auth`` block supports the following properties: + + ``key-management`` (scalar) + : The supported key management modes are ``none`` (no key management); + ``psk`` (WPA with pre-shared key, common for home wifi); ``eap`` (WPA + with EAP, common for enterprise wifi); and ``802.1x`` (used primarily + for wired Ethernet connections). + + ``password`` (scalar) + : The password string for EAP, or the pre-shared key for WPA-PSK. + + The following properties can be used if ``key-management`` is ``eap`` + or ``802.1x``: + + ``method`` (scalar) + : The EAP method to use. The supported EAP methods are ``tls`` (TLS), + ``peap`` (Protected EAP), and ``ttls`` (Tunneled TLS). + + ``identity`` (scalar) + : The identity to use for EAP. + + ``anonymous-identity`` (scalar) + : The identity to pass over the unencrypted channel if the chosen EAP + method supports passing a different tunnelled identity. + + ``ca-certificate`` (scalar) + : Path to a file with one or more trusted certificate authority (CA) + certificates. + + ``client-certificate`` (scalar) + : Path to a file containing the certificate to be used by the client + during authentication. + + ``client-key`` (scalar) + : Path to a file containing the private key corresponding to + ``client-certificate``. + + ``client-key-password`` (scalar) + : Password to use to decrypt the private key specified in + ``client-key`` if it is encrypted. + + ``phase2-auth`` (scalar) – since **0.99** + : Phase 2 authentication mechanism. + + +## Properties for device type ``ethernets:`` +Ethernet device definitions, beyond common ones described above, also support +some additional properties that can be used for SR-IOV devices. + +``link`` (scalar) – since **0.99** + +: (SR-IOV devices only) The ``link`` property declares the device as a + Virtual Function of the selected Physical Function device, as identified + by the given netplan id. + +Example: + + ethernets: + enp1: {...} + enp1s16f1: + link: enp1 + +``virtual-function-count`` (scalar) – since **0.99** + +: (SR-IOV devices only) In certain special cases VFs might need to be + configured outside of netplan. For such configurations ``virtual-function-count`` + can be optionally used to set an explicit number of Virtual Functions for + the given Physical Function. If unset, the default is to create only as many + VFs as are defined in the netplan configuration. This should be used for special + cases only. + + **Requires feature: sriov** + +## Properties for device type ``modems:`` +GSM/CDMA modem configuration is only supported for the ``NetworkManager`` +backend. ``systemd-networkd`` does not support modems. + +**Requires feature: modems** + +``apn`` (scalar) – since **0.99** + +: Set the carrier APN (Access Point Name). This can be omitted if + ``auto-config`` is enabled. + +``auto-config`` (bool) – since **0.99** + +: Specify whether to try and autoconfigure the modem by doing a lookup of + the carrier against the Mobile Broadband Provider database. This may not + work for all carriers. + +``device-id`` (scalar) – since **0.99** + +: Specify the device ID (as given by the WWAN management service) of the + modem to match. This can be found using ``mmcli``. + +``network-id`` (scalar) – since **0.99** + +: Specify the Network ID (GSM LAI format). If this is specified, the device + will not roam networks. + +``number`` (scalar) – since **0.99** + +: The number to dial to establish the connection to the mobile broadband + network. (Deprecated for GSM) + +``password`` (scalar) – since **0.99** + +: Specify the password used to authenticate with the carrier network. This + can be omitted if ``auto-config`` is enabled. + +``pin`` (scalar) – since **0.99** + +: Specify the SIM PIN to allow it to operate if a PIN is set. + +``sim-id`` (scalar) – since **0.99** + +: Specify the SIM unique identifier (as given by the WWAN management service) + which this connection applies to. If given, the connection will apply to + any device also allowed by ``device-id`` which contains a SIM card matching + the given identifier. + +``sim-operator-id`` (scalar) – since **0.99** + +: Specify the MCC/MNC string (such as "310260" or "21601") which identifies + the carrier that this connection should apply to. If given, the connection + will apply to any device also allowed by ``device-id`` and ``sim-id`` + which contains a SIM card provisioned by the given operator. + +``username`` (scalar) – since **0.99** + +: Specify the username used to authentiate with the carrier network. This + can be omitted if ``auto-config`` is enabled. + +## Properties for device type ``wifis:`` +Note that ``systemd-networkd`` does not natively support wifi, so you need +wpasupplicant installed if you let the ``networkd`` renderer handle wifi. + +``access-points`` (mapping) + +: This provides pre-configured connections to NetworkManager. Note that + users can of course select other access points/SSIDs. The keys of the + mapping are the SSIDs, and the values are mappings with the following + supported properties: + + ``password`` (scalar) + : Enable WPA2 authentication and set the passphrase for it. If neither + this nor an ``auth`` block are given, the network is assumed to be + open. The setting + + password: "S3kr1t" + + is equivalent to + + auth: + key-management: psk + password: "S3kr1t" + + ``mode`` (scalar) + : Possible access point modes are ``infrastructure`` (the default), + ``ap`` (create an access point to which other devices can connect), + and ``adhoc`` (peer to peer networks without a central access point). + ``ap`` is only supported with NetworkManager. + + ``bssid`` (scalar) – since **0.99** + : If specified, directs the device to only associate with the given + access point. + + ``band`` (scalar) – since **0.99** + : Possible bands are ``5GHz`` (for 5GHz 802.11a) and ``2.4GHz`` + (for 2.4GHz 802.11), do not restrict the 802.11 frequency band of the + network if unset (the default). + + ``channel`` (scalar) – since **0.99** + : Wireless channel to use for the Wi-Fi connection. Because channel + numbers overlap between bands, this property takes effect only if + the ``band`` property is also set. + + ``hidden`` (bool) – since **0.100** + : Set to ``true`` to change the SSID scan technique for connecting to + hidden WiFi networks. Note this may have slower performance compared + to ``false`` (the default) when connecting to publicly broadcast + SSIDs. + +``wakeonwlan`` (sequence of scalars) – since **0.99** + +: This enables WakeOnWLan on supported devices. Not all drivers support all + options. May be any combination of ``any``, ``disconnect``, ``magic_pkt``, + ``gtk_rekey_failure``, ``eap_identity_req``, ``four_way_handshake``, + ``rfkill_release`` or ``tcp`` (NetworkManager only). Or the exclusive + ``default`` flag (the default). + +## Properties for device type ``bridges:`` + +``interfaces`` (sequence of scalars) + +: All devices matching this ID list will be added to the bridge. This may + be an empty list, in which case the bridge will be brought online with + no member interfaces. + + Example: + + ethernets: + switchports: + match: {name: "enp2*"} + [...] + bridges: + br0: + interfaces: [switchports] + +``parameters`` (mapping) + +: Customization parameters for special bridging options. Time intervals + may need to be expressed as a number of seconds or milliseconds: the + default value type is specified below. If necessary, time intervals can + be qualified using a time suffix (such as "s" for seconds, "ms" for + milliseconds) to allow for more control over its behavior. + + ``ageing-time`` (scalar) + : Set the period of time to keep a MAC address in the forwarding + database after a packet is received. This maps to the AgeingTimeSec= + property when the networkd renderer is used. If no time suffix is + specified, the value will be interpreted as seconds. + + ``priority`` (scalar) + : Set the priority value for the bridge. This value should be a + number between ``0`` and ``65535``. Lower values mean higher + priority. The bridge with the higher priority will be elected as + the root bridge. + + ``port-priority`` (scalar) + : Set the port priority to . The priority value is + a number between ``0`` and ``63``. This metric is used in the + designated port and root port selection algorithms. + + ``forward-delay`` (scalar) + : Specify the period of time the bridge will remain in Listening and + Learning states before getting to the Forwarding state. This field + maps to the ForwardDelaySec= property for the networkd renderer. + If no time suffix is specified, the value will be interpreted as + seconds. + + ``hello-time`` (scalar) + : Specify the interval between two hello packets being sent out from + the root and designated bridges. Hello packets communicate + information about the network topology. When the networkd renderer + is used, this maps to the HelloTimeSec= property. If no time suffix + is specified, the value will be interpreted as seconds. + + ``max-age`` (scalar) + : Set the maximum age of a hello packet. If the last hello packet is + older than that value, the bridge will attempt to become the root + bridge. This maps to the MaxAgeSec= property when the networkd + renderer is used. If no time suffix is specified, the value will be + interpreted as seconds. + + ``path-cost`` (scalar) + : Set the cost of a path on the bridge. Faster interfaces should have + a lower cost. This allows a finer control on the network topology + so that the fastest paths are available whenever possible. + + ``stp`` (bool) + : Define whether the bridge should use Spanning Tree Protocol. The + default value is "true", which means that Spanning Tree should be + used. + + +## Properties for device type ``bonds:`` + +``interfaces`` (sequence of scalars) + +: All devices matching this ID list will be added to the bond. + + Example: + + ethernets: + switchports: + match: {name: "enp2*"} + [...] + bonds: + bond0: + interfaces: [switchports] + +``parameters`` (mapping) + +: Customization parameters for special bonding options. Time intervals + may need to be expressed as a number of seconds or milliseconds: the + default value type is specified below. If necessary, time intervals can + be qualified using a time suffix (such as "s" for seconds, "ms" for + milliseconds) to allow for more control over its behavior. + + ``mode`` (scalar) + : Set the bonding mode used for the interfaces. The default is + ``balance-rr`` (round robin). Possible values are ``balance-rr``, + ``active-backup``, ``balance-xor``, ``broadcast``, ``802.3ad``, + ``balance-tlb``, and ``balance-alb``. + For OpenVSwitch ``active-backup`` and the additional modes + ``balance-tcp`` and ``balance-slb`` are supported. + + ``lacp-rate`` (scalar) + : Set the rate at which LACPDUs are transmitted. This is only useful + in 802.3ad mode. Possible values are ``slow`` (30 seconds, default), + and ``fast`` (every second). + + ``mii-monitor-interval`` (scalar) + : Specifies the interval for MII monitoring (verifying if an interface + of the bond has carrier). The default is ``0``; which disables MII + monitoring. This is equivalent to the MIIMonitorSec= field for the + networkd backend. If no time suffix is specified, the value will be + interpreted as milliseconds. + + ``min-links`` (scalar) + : The minimum number of links up in a bond to consider the bond + interface to be up. + + ``transmit-hash-policy`` (scalar) + : Specifies the transmit hash policy for the selection of slaves. This + is only useful in balance-xor, 802.3ad and balance-tlb modes. + Possible values are ``layer2``, ``layer3+4``, ``layer2+3``, + ``encap2+3``, and ``encap3+4``. + + ``ad-select`` (scalar) + : Set the aggregation selection mode. Possible values are ``stable``, + ``bandwidth``, and ``count``. This option is only used in 802.3ad + mode. + + ``all-slaves-active`` (bool) + : If the bond should drop duplicate frames received on inactive ports, + set this option to ``false``. If they should be delivered, set this + option to ``true``. The default value is false, and is the desirable + behavior in most situations. + + ``arp-interval`` (scalar) + : Set the interval value for how frequently ARP link monitoring should + happen. The default value is ``0``, which disables ARP monitoring. + For the networkd backend, this maps to the ARPIntervalSec= property. + If no time suffix is specified, the value will be interpreted as + milliseconds. + + ``arp-ip-targets`` (sequence of scalars) + : IPs of other hosts on the link which should be sent ARP requests in + order to validate that a slave is up. This option is only used when + ``arp-interval`` is set to a value other than ``0``. At least one IP + address must be given for ARP link monitoring to function. Only IPv4 + addresses are supported. You can specify up to 16 IP addresses. The + default value is an empty list. + + ``arp-validate`` (scalar) + : Configure how ARP replies are to be validated when using ARP link + monitoring. Possible values are ``none``, ``active``, ``backup``, + and ``all``. + + ``arp-all-targets`` (scalar) + : Specify whether to use any ARP IP target being up as sufficient for + a slave to be considered up; or if all the targets must be up. This + is only used for ``active-backup`` mode when ``arp-validate`` is + enabled. Possible values are ``any`` and ``all``. + + ``up-delay`` (scalar) + : Specify the delay before enabling a link once the link is physically + up. The default value is ``0``. This maps to the UpDelaySec= property + for the networkd renderer. This option is only valid for the miimon + link monitor. If no time suffix is specified, the value will be + interpreted as milliseconds. + + ``down-delay`` (scalar) + : Specify the delay before disabling a link once the link has been + lost. The default value is ``0``. This maps to the DownDelaySec= + property for the networkd renderer. This option is only valid for the + miimon link monitor. If no time suffix is specified, the value will + be interpreted as milliseconds. + + ``fail-over-mac-policy`` (scalar) + : Set whether to set all slaves to the same MAC address when adding + them to the bond, or how else the system should handle MAC addresses. + The possible values are ``none``, ``active``, and ``follow``. + + ``gratuitous-arp`` (scalar) + : Specify how many ARP packets to send after failover. Once a link is + up on a new slave, a notification is sent and possibly repeated if + this value is set to a number greater than ``1``. The default value + is ``1`` and valid values are between ``1`` and ``255``. This only + affects ``active-backup`` mode. + + For historical reasons, the misspelling ``gratuitious-arp`` is also + accepted and has the same function. + + ``packets-per-slave`` (scalar) + : In ``balance-rr`` mode, specifies the number of packets to transmit + on a slave before switching to the next. When this value is set to + ``0``, slaves are chosen at random. Allowable values are between + ``0`` and ``65535``. The default value is ``1``. This setting is + only used in ``balance-rr`` mode. + + ``primary-reselect-policy`` (scalar) + : Set the reselection policy for the primary slave. On failure of the + active slave, the system will use this policy to decide how the new + active slave will be chosen and how recovery will be handled. The + possible values are ``always``, ``better``, and ``failure``. + + ``resend-igmp`` (scalar) + : In modes ``balance-rr``, ``active-backup``, ``balance-tlb`` and + ``balance-alb``, a failover can switch IGMP traffic from one + slave to another. + + This parameter specifies how many IGMP membership reports + are issued on a failover event. Values range from 0 to 255. 0 + disables sending membership reports. Otherwise, the first + membership report is sent on failover and subsequent reports + are sent at 200ms intervals. + + ``learn-packet-interval`` (scalar) + : Specify the interval between sending learning packets to + each slave. The value range is between ``1`` and ``0x7fffffff``. + The default value is ``1``. This option only affects ``balance-tlb`` + and ``balance-alb`` modes. Using the networkd renderer, this field + maps to the LearnPacketIntervalSec= property. If no time suffix is + specified, the value will be interpreted as seconds. + + ``primary`` (scalar) + : Specify a device to be used as a primary slave, or preferred device + to use as a slave for the bond (ie. the preferred device to send + data through), whenever it is available. This only affects + ``active-backup``, ``balance-alb``, and ``balance-tlb`` modes. + + +## Properties for device type ``tunnels:`` + +Tunnels allow traffic to pass as if it was between systems on the same local +network, although systems may be far from each other but reachable via the +Internet. They may be used to support IPv6 traffic on a network where the ISP +does not provide the service, or to extend and "connect" separate local +networks. Please see https://en.wikipedia.org/wiki/Tunneling_protocol for +more general information about tunnels. + +``mode`` (scalar) + +: Defines the tunnel mode. Valid options are ``sit``, ``gre``, ``ip6gre``, + ``ipip``, ``ipip6``, ``ip6ip6``, ``vti``, ``vti6`` and ``wireguard``. + Additionally, the ``networkd`` backend also supports ``gretap`` and + ``ip6gretap`` modes. + In addition, the ``NetworkManager`` backend supports ``isatap`` tunnels. + +``local`` (scalar) + +: Defines the address of the local endpoint of the tunnel. + +``remote`` (scalar) + +: Defines the address of the remote endpoint of the tunnel. + +``ttl`` (scalar) – since **0.103** + +: Defines the TTL of the tunnel. + +``key`` (scalar or mapping) + +: Define keys to use for the tunnel. The key can be a number or a dotted + quad (an IPv4 address). For ``wireguard`` it can be a base64-encoded + private key or (as of ``networkd`` v242+) an absolute path to a file, + containing the private key (since 0.100). + It is used for identification of IP transforms. This is only required + for ``vti`` and ``vti6`` when using the networkd backend, and for + ``gre`` or ``ip6gre`` tunnels when using the NetworkManager backend. + + This field may be used as a scalar (meaning that a single key is + specified and to be used for input, output and private key), or as a + mapping, where you can further specify ``input``/``output``/``private``. + + ``input`` (scalar) + : The input key for the tunnel + + ``output`` (scalar) + : The output key for the tunnel + + ``private`` (scalar) – since **0.100** + : A base64-encoded private key required for Wireguard tunnels. When the + ``systemd-networkd`` backend (v242+) is used, this can also be an + absolute path to a file containing the private key. + +``keys`` (scalar or mapping) + +: Alternate name for the ``key`` field. See above. + +Examples: + + tunnels: + tun0: + mode: gre + local: ... + remote: ... + keys: + input: 1234 + output: 5678 + + tunnels: + tun0: + mode: vti6 + local: ... + remote: ... + key: 59568549 + + tunnels: + wg0: + mode: wireguard + addresses: [...] + peers: + - keys: + public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= + shared: /path/to/shared.key + ... + key: mNb7OIIXTdgW4khM7OFlzJ+UPs7lmcWHV7xjPgakMkQ= + + tunnels: + wg0: + mode: wireguard + addresses: [...] + peers: + - keys: + public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= + ... + keys: + private: /path/to/priv.key + + +Wireguard specific keys: + +``mark`` (scalar) – since **0.100** + +: Firewall mark for outgoing WireGuard packets from this interface, + optional. + +``port`` (scalar) – since **0.100** + +: UDP port to listen at or ``auto``. Optional, defaults to ``auto``. + +``peers`` (sequence of mappings) – since **0.100** + +: A list of peers, each having keys documented below. + +Example: + + tunnels: + wg0: + mode: wireguard + key: /path/to/private.key + mark: 42 + port: 5182 + peers: + - keys: + public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= + allowed-ips: [0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"] + keepalive: 23 + endpoint: 1.2.3.4:5 + - keys: + public: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= + shared: /some/shared.key + allowed-ips: [10.10.10.20/24] + keepalive: 22 + endpoint: 5.4.3.2:1 + +``endpoint`` (scalar) – since **0.100** + +: Remote endpoint IPv4/IPv6 address or a hostname, followed by a colon + and a port number. + +``allowed-ips`` (sequence of scalars) – since **0.100** + +: A list of IP (v4 or v6) addresses with CIDR masks from which this peer + is allowed to send incoming traffic and to which outgoing traffic for + this peer is directed. The catch-all 0.0.0.0/0 may be specified for + matching all IPv4 addresses, and ::/0 may be specified for matching + all IPv6 addresses. + +``keepalive`` (scalar) – since **0.100** + +: An interval in seconds, between 1 and 65535 inclusive, of how often to + send an authenticated empty packet to the peer for the purpose of + keeping a stateful firewall or NAT mapping valid persistently. Optional. + +``keys`` (mapping) – since **0.100** + +: Define keys to use for the Wireguard peers. + + This field can be used as a mapping, where you can further specify the + ``public`` and ``shared`` keys. + + ``public`` (scalar) – since **0.100** + : A base64-encoded public key, required for Wireguard peers. + + ``shared`` (scalar) – since **0.100** + : A base64-encoded preshared key. Optional for Wireguard peers. + When the ``systemd-networkd`` backend (v242+) is used, this can + also be an absolute path to a file containing the preshared key. + +## Properties for device type ``vlans:`` + +``id`` (scalar) + +: VLAN ID, a number between 0 and 4094. + +``link`` (scalar) + +: netplan ID of the underlying device definition on which this VLAN gets + created. + +Example: + + ethernets: + eno1: {...} + vlans: + en-intra: + id: 1 + link: eno1 + dhcp4: yes + en-vpn: + id: 2 + link: eno1 + addresses: ... + +## Properties for device type ``nm-devices:`` + +The ``nm-devices`` device type is for internal use only and should not be used in normal configuration files. It enables a fallback mode for unsupported settings, using the ``passthrough`` mapping. + + +## Backend-specific configuration parameters + +In addition to the other fields available to configure interfaces, some +backends may require to record some of their own parameters in netplan, +especially if the netplan definitions are generated automatically by the +consumer of that backend. Currently, this is only used with ``NetworkManager``. + +``networkmanager`` (mapping) – since **0.99** + +: Keeps the NetworkManager-specific configuration parameters used by the + daemon to recognize connections. + + ``name`` (scalar) – since **0.99** + : Set the display name for the connection. + + ``uuid`` (scalar) – since **0.99** + : Defines the UUID (unique identifier) for this connection, as + generated by NetworkManager itself. + + ``stable-id`` (scalar) – since **0.99** + : Defines the stable ID (a different form of a connection name) used + by NetworkManager in case the name of the connection might otherwise + change, such as when sharing connections between users. + + ``device`` (scalar) – since **0.99** + : Defines the interface name for which this connection applies. + + ``passthrough`` (mapping) – since **0.102** + : Can be used as a fallback mechanism to missing keyfile settings. + +## Examples +Configure an ethernet device with networkd, identified by its name, and enable +DHCP: + + network: + version: 2 + ethernets: + eno1: + dhcp4: true + +This is an example of a static-configured interface with multiple IPv4 addresses +and multiple gateways with networkd, with equal route metric levels, and static +DNS nameservers (Google DNS for this example): + + network: + version: 2 + renderer: networkd + ethernets: + eno1: + addresses: + - 10.0.0.10/24 + - 11.0.0.11/24 + nameservers: + addresses: + - 8.8.8.8 + - 8.8.4.4 + routes: + - to: 0.0.0.0/0 + via: 10.0.0.1 + metric: 100 + - to: 0.0.0.0/0 + via: 11.0.0.1 + metric: 100 + +This is a complex example which shows most available features: + + network: + version: 2 + # if specified, can only realistically have that value, as networkd cannot + # render wifi/3G. + renderer: NetworkManager + ethernets: + # opaque ID for physical interfaces, only referred to by other stanzas + id0: + match: + macaddress: 00:11:22:33:44:55 + wakeonlan: true + dhcp4: true + addresses: + - 192.168.14.2/24 + - 192.168.14.3/24 + - "2001:1::1/64" + nameservers: + search: [foo.local, bar.local] + addresses: [8.8.8.8] + routes: + - to: default + via: 192.168.14.1 + - to: default + via: "2001:1::2" + - to: 0.0.0.0/0 + via: 11.0.0.1 + table: 70 + on-link: true + metric: 3 + routing-policy: + - to: 10.0.0.0/8 + from: 192.168.14.2/24 + table: 70 + priority: 100 + - to: 20.0.0.0/8 + from: 192.168.14.3/24 + table: 70 + priority: 50 + # only networkd can render on-link routes and routing policies + renderer: networkd + lom: + match: + driver: ixgbe + # you are responsible for setting tight enough match rules + # that only match one device if you use set-name + set-name: lom1 + dhcp6: true + switchports: + # all cards on second PCI bus unconfigured by + # themselves, will be added to br0 below + match: + name: enp2* + mtu: 1280 + wifis: + all-wlans: + # useful on a system where you know there is + # only ever going to be one device + match: {} + access-points: + "Joe's home": + # mode defaults to "infrastructure" (client) + password: "s3kr1t" + # this creates an AP on wlp1s0 using hostapd + # no match rules, thus the ID is the interface name + wlp1s0: + access-points: + "guest": + mode: ap + # no WPA config implies default of open + bridges: + # the key name is the name for virtual (created) interfaces + # no match: and set-name: allowed + br0: + # IDs of the components; switchports expands into multiple interfaces + interfaces: [wlp1s0, switchports] + dhcp4: true + + diff --git a/examples/bonding.yaml b/examples/bonding.yaml new file mode 100644 index 0000000..26adaf8 --- /dev/null +++ b/examples/bonding.yaml @@ -0,0 +1,12 @@ +network: + version: 2 + renderer: networkd + bonds: + bond0: + dhcp4: yes + interfaces: + - enp3s0 + - enp4s0 + parameters: + mode: active-backup + primary: enp3s0 diff --git a/examples/bonding_router.yaml b/examples/bonding_router.yaml new file mode 100644 index 0000000..f20eadb --- /dev/null +++ b/examples/bonding_router.yaml @@ -0,0 +1,46 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp1s0: + dhcp4: no + enp2s0: + dhcp4: no + enp3s0: + dhcp4: no + optional: true + enp4s0: + dhcp4: no + optional: true + enp5s0: + dhcp4: no + optional: true + enp6s0: + dhcp4: no + optional: true + bonds: + bond-lan: + interfaces: [enp2s0, enp3s0] + addresses: [192.168.93.2/24] + parameters: + mode: 802.3ad + mii-monitor-interval: 1 + bond-wan: + interfaces: [enp1s0, enp4s0] + addresses: [192.168.1.252/24] + nameservers: + search: [local] + addresses: [8.8.8.8, 8.8.4.4] + parameters: + mode: active-backup + mii-monitor-interval: 1 + gratuitious-arp: 5 + routes: + - to: default + via: 192.168.1.1 + bond-conntrack: + interfaces: [enp5s0, enp6s0] + addresses: [192.168.254.2/24] + parameters: + mode: balance-rr + mii-monitor-interval: 1 diff --git a/examples/bridge.yaml b/examples/bridge.yaml new file mode 100644 index 0000000..dbfcae5 --- /dev/null +++ b/examples/bridge.yaml @@ -0,0 +1,11 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: + dhcp4: no + bridges: + br0: + dhcp4: yes + interfaces: + - enp3s0 diff --git a/examples/bridge_vlan.yaml b/examples/bridge_vlan.yaml new file mode 100644 index 0000000..b917b84 --- /dev/null +++ b/examples/bridge_vlan.yaml @@ -0,0 +1,15 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp0s25: + dhcp4: true + bridges: + br0: + addresses: [ 10.3.99.25/24 ] + interfaces: [ vlan15 ] + vlans: + vlan15: + accept-ra: no + id: 15 + link: enp0s25 diff --git a/examples/dbus_config_scenario.txt b/examples/dbus_config_scenario.txt new file mode 100644 index 0000000..d1ec15e --- /dev/null +++ b/examples/dbus_config_scenario.txt @@ -0,0 +1,41 @@ +# Example interaction with Netplan's DBus config API + +## Copy the current state from /{etc,run,lib}/netplan/*.yaml by creating a new config object +$ busctl call io.netplan.Netplan /io/netplan/Netplan io.netplan.Netplan Config +o "/io/netplan/Netplan/config/ULJIU0" + +## Read the merged YAML configuration +$ busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 io.netplan.Netplan.Config Get +s "network:\n ethernets:\n eth0:\n dhcp4: true\n renderer: networkd\n version: 2\n" + +## Write a new config snippet into 70-snapd.yaml +$ busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 io.netplan.Netplan.Config Set ss "ethernets.eth0={dhcp4: false, dhcp6: true}" "70-snapd" +b true + +## Check the newly written configuration +$ busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 io.netplan.Netplan.Config Get +s "network:\n ethernets:\n eth0:\n dhcp4: false\n dhcp6: true\n renderer: networkd\n version: 2\n" + +## Try to apply the current config object's state +$ busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 io.netplan.Netplan.Config Try u 20 +b true + +## Accept the Try() state within the 20 seconds timeout, if not it will be auto-rejected +$ busctl call io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 io.netplan.Netplan.Config Apply +b true + +[SIGNAL] io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 io.netplan.Netplan.Config Changed() is triggered +[OBJECT] io.netplan.Netplan /io/netplan/Netplan/config/ULJIU0 is removed from the bus + +## Create a new config object and get the merged YAML config +$ busctl call io.netplan.Netplan /io/netplan/Netplan io.netplan.Netplan Config +o "/io/netplan/Netplan/config/KC0IU0 +$ busctl call io.netplan.Netplan /io/netplan/Netplan/config/KC0IU0 io.netplan.Netplan.Config Get +s "network:\n ethernets:\n eth0:\n dhcp4: false\n dhcp6: true\n renderer: networkd\n version: 2\n" + +## Reject that config object again +$ busctl call io.netplan.Netplan /io/netplan/Netplan/config/KC0IU0 io.netplan.Netplan.Config Cancel +b true + +[SIGNAL] io.netplan.Netplan /io/netplan/Netplan/config/KC0IU0 io.netplan.Netplan.Config Changed() is triggered +[OBJECT] io.netplan.Netplan /io/netplan/Netplan/config/KC0IU0 is removed from the bus diff --git a/examples/dhcp.yaml b/examples/dhcp.yaml new file mode 100644 index 0000000..f7f85ef --- /dev/null +++ b/examples/dhcp.yaml @@ -0,0 +1,6 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: + dhcp4: true diff --git a/examples/dhcp_wired8021x.yaml b/examples/dhcp_wired8021x.yaml new file mode 100644 index 0000000..9f401dd --- /dev/null +++ b/examples/dhcp_wired8021x.yaml @@ -0,0 +1,11 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: + dhcp4: true + auth: + key-management: 802.1x + method: ttls + identity: fluffy@cisco.com + password: hash:83...11 diff --git a/examples/direct_connect_gateway.yaml b/examples/direct_connect_gateway.yaml new file mode 100644 index 0000000..6eac9cd --- /dev/null +++ b/examples/direct_connect_gateway.yaml @@ -0,0 +1,9 @@ +network: + version: 2 + renderer: networkd + ethernets: + addresses: [ "10.10.10.1/24" ] + routes: + - to: 0.0.0.0/0 + via: 9.9.9.9 + on-link: true diff --git a/examples/direct_connect_gateway_ipv6.yaml b/examples/direct_connect_gateway_ipv6.yaml new file mode 100644 index 0000000..3f821d3 --- /dev/null +++ b/examples/direct_connect_gateway_ipv6.yaml @@ -0,0 +1,11 @@ +network: + version: 2 + renderer: networkd + ethernets: + addresses: [ "2001:cafe:face:beef::dead:dead/64" ] + routes: + - to: "2001:cafe:face::1/128" + scope: link + - to: "::/0" + via: "2001:cafe:face::1" + on-link: true diff --git a/examples/ipv6_tunnel.yaml b/examples/ipv6_tunnel.yaml new file mode 100644 index 0000000..222691b --- /dev/null +++ b/examples/ipv6_tunnel.yaml @@ -0,0 +1,20 @@ +network: + version: 2 + ethernets: + eth0: + addresses: + - 1.1.1.1/24 + - "2001:cafe:face::1/64" + routes: + - to: default + via: 1.1.1.254 + tunnels: + he-ipv6: + mode: sit + remote: 2.2.2.2 + local: 1.1.1.1 + addresses: + - "2001:dead:beef::2/64" + routes: + - to: default + via: "2001:dead:beef::1" diff --git a/examples/loopback_interface.yaml b/examples/loopback_interface.yaml new file mode 100644 index 0000000..734f091 --- /dev/null +++ b/examples/loopback_interface.yaml @@ -0,0 +1,8 @@ +network: + version: 2 + renderer: networkd + ethernets: + lo: + match: + name: lo + addresses: [ 7.7.7.7/32 ] diff --git a/examples/modem.yaml b/examples/modem.yaml new file mode 100644 index 0000000..043d74a --- /dev/null +++ b/examples/modem.yaml @@ -0,0 +1,15 @@ +network: + version: 2 + renderer: NetworkManager + modems: + cdc-wdm1: + mtu: 1600 + apn: ISP.CINGULAR + username: ISP@CINGULARGPRS.COM + password: CINGULAR1 + number: "*99#" + network-id: 24005 + device-id: da812de91eec16620b06cd0ca5cbc7ea25245222 + pin: 2345 + sim-id: 89148000000060671234 + sim-operator-id: 310260 diff --git a/examples/network_manager.yaml b/examples/network_manager.yaml new file mode 100644 index 0000000..b654768 --- /dev/null +++ b/examples/network_manager.yaml @@ -0,0 +1,3 @@ +network: + version: 2 + renderer: NetworkManager diff --git a/examples/openvswitch.yaml b/examples/openvswitch.yaml new file mode 100644 index 0000000..678e155 --- /dev/null +++ b/examples/openvswitch.yaml @@ -0,0 +1,45 @@ +network: + version: 2 + openvswitch: + protocols: [OpenFlow13, OpenFlow14, OpenFlow15] + ports: + - [patch0-1, patch1-0] + ssl: + ca-cert: /some/ca-cert.pem + certificate: /another/cert.pem + private-key: /private/key.pem + external-ids: + somekey: somevalue + other-config: + key: value + ethernets: + eth0: + addresses: [10.5.32.26/20] + openvswitch: + external-ids: + iface-id: mylocaliface + other-config: + disable-in-band: false + eth1: {} + bonds: + bond0: + interfaces: [patch1-0, eth1] + openvswitch: + lacp: passive + parameters: + mode: balance-tcp + bridges: + ovs0: + addresses: [10.5.48.11/20] + interfaces: [patch0-1, eth0, bond0] + openvswitch: + protocols: [OpenFlow10, OpenFlow11, OpenFlow12] + controller: + addresses: [unix:/var/run/openvswitch/ovs0.mgmt] + connection-mode: out-of-band + fail-mode: secure + mcast-snooping: true + external-ids: + iface-id: myhostname + other-config: + disable-in-band: true diff --git a/examples/route_metric.yaml b/examples/route_metric.yaml new file mode 100644 index 0000000..20bf48c --- /dev/null +++ b/examples/route_metric.yaml @@ -0,0 +1,11 @@ +network: + version: 2 + ethernets: + enred: + dhcp4: yes + dhcp4-overrides: + route-metric: 100 + engreen: + dhcp4: yes + dhcp4-overrides: + route-metric: 200 diff --git a/examples/source_routing.yaml b/examples/source_routing.yaml new file mode 100644 index 0000000..f56ae6b --- /dev/null +++ b/examples/source_routing.yaml @@ -0,0 +1,28 @@ +network: + version: 2 + renderer: networkd + ethernets: + ens3: + addresses: + - 192.168.3.30/24 + dhcp4: no + routes: + - to: 192.168.3.0/24 + via: 192.168.3.1 + table: 101 + routing-policy: + - from: 192.168.3.0/24 + table: 101 + ens5: + addresses: + - 192.168.5.24/24 + dhcp4: no + routes: + - to: default + via: 192.168.5.1 + - to: 192.168.5.0/24 + via: 192.168.5.1 + table: 102 + routing-policy: + - from: 192.168.5.0/24 + table: 102 diff --git a/examples/sriov.yaml b/examples/sriov.yaml new file mode 100644 index 0000000..67de132 --- /dev/null +++ b/examples/sriov.yaml @@ -0,0 +1,14 @@ +network: + version: 2 + renderer: networkd + ethernets: + eno1: + mtu: 9000 + enp1s16f1: + link: eno1 + addresses : [ "10.15.98.25/24" ] + vf1: + match: + name: enp1s16f[2-3] + link: eno1 + addresses : [ "10.15.99.25/24" ] diff --git a/examples/sriov_vlan.yaml b/examples/sriov_vlan.yaml new file mode 100644 index 0000000..2c664d7 --- /dev/null +++ b/examples/sriov_vlan.yaml @@ -0,0 +1,18 @@ +network: + version: 2 + renderer: networkd + ethernets: + eno1: + mtu: 9000 + enp1s16f1: + link: eno1 + addresses : [ "10.15.98.25/24" ] + vlans: + vlan1: + id: 15 + link: enp1s16f1 + addresses: [ "10.3.99.5/24" ] + vlan2_hw: + id: 10 + link: enp1s16f1 + renderer: sriov diff --git a/examples/static.yaml b/examples/static.yaml new file mode 100644 index 0000000..e4c0678 --- /dev/null +++ b/examples/static.yaml @@ -0,0 +1,13 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: + addresses: + - 10.10.10.2/24 + nameservers: + search: [mydomain, otherdomain] + addresses: [10.10.10.1, 1.1.1.1] + routes: + - to: default + via: 10.10.10.1 diff --git a/examples/static_multiaddress.yaml b/examples/static_multiaddress.yaml new file mode 100644 index 0000000..de2be06 --- /dev/null +++ b/examples/static_multiaddress.yaml @@ -0,0 +1,11 @@ +network: + version: 2 + renderer: networkd + ethernets: + enp3s0: + addresses: + - 10.100.1.38/24 + - 10.100.1.39/24 + routes: + - to: default + via: 10.100.1.1 diff --git a/examples/static_singlenic_multiip_multigateway.yaml b/examples/static_singlenic_multiip_multigateway.yaml new file mode 100644 index 0000000..c9b8de4 --- /dev/null +++ b/examples/static_singlenic_multiip_multigateway.yaml @@ -0,0 +1,19 @@ +network: + version: 2 + renderer: networkd + ethernets: + eno1: + addresses: + - 10.0.0.10/24 + - 11.0.0.11/24 + nameservers: + addresses: + - 8.8.8.8 + - 8.8.4.4 + routes: + - to: 0.0.0.0/0 + via: 10.0.0.1 + metric: 100 + - to: 0.0.0.0/0 + via: 11.0.0.1 + metric: 100 diff --git a/examples/vlan.yaml b/examples/vlan.yaml new file mode 100644 index 0000000..24af0b2 --- /dev/null +++ b/examples/vlan.yaml @@ -0,0 +1,27 @@ +network: + version: 2 + renderer: networkd + ethernets: + mainif: + match: + macaddress: "de:ad:be:ef:ca:fe" + set-name: mainif + addresses: [ "10.3.0.5/23" ] + nameservers: + addresses: [ "8.8.8.8", "8.8.4.4" ] + search: [ example.com ] + routes: + - to: default + via: 10.3.0.1 + vlans: + vlan15: + id: 15 + link: mainif + addresses: [ "10.3.99.5/24" ] + vlan10: + id: 10 + link: mainif + addresses: [ "10.3.98.5/24" ] + nameservers: + addresses: [ "127.0.0.1" ] + search: [ domain1.example.com, domain2.example.com ] diff --git a/examples/windows_dhcp_server.yaml b/examples/windows_dhcp_server.yaml new file mode 100644 index 0000000..b4a178d --- /dev/null +++ b/examples/windows_dhcp_server.yaml @@ -0,0 +1,6 @@ +network: + version: 2 + ethernets: + enp3s0: + dhcp4: yes + dhcp-identifier: mac diff --git a/examples/wireguard.yaml b/examples/wireguard.yaml new file mode 100644 index 0000000..6b745d1 --- /dev/null +++ b/examples/wireguard.yaml @@ -0,0 +1,31 @@ +network: + version: 2 + tunnels: + wg0: #server + mode: wireguard + addresses: [10.10.10.20/24] + key: 4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8= + mark: 42 + port: 51820 + peers: + - keys: + public: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= + shared: 7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= + allowed-ips: [20.20.20.10/24] + routes: + - to: default + via: 10.10.10.21 + wg1: #client + mode: wireguard + addresses: [20.20.20.10/24] + key: KPt9BzQjejRerEv8RMaFlpsD675gNexELOQRXt/AcH0= + peers: + - endpoint: 10.10.10.20:51820 + allowed-ips: [0.0.0.0/0] + keys: + public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= + shared: 7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= + keepalive: 21 + routes: + - to: default + via: 20.20.20.11 diff --git a/examples/wireless.yaml b/examples/wireless.yaml new file mode 100644 index 0000000..a7d82ad --- /dev/null +++ b/examples/wireless.yaml @@ -0,0 +1,16 @@ +network: + version: 2 + renderer: networkd + wifis: + wlp2s0b1: + dhcp4: no + dhcp6: no + addresses: [192.168.0.21/24] + nameservers: + addresses: [192.168.0.1, 8.8.8.8] + access-points: + "network_ssid_name": + password: "**********" + routes: + - to: default + via: 192.168.0.1 diff --git a/examples/wpa_enterprise.yaml b/examples/wpa_enterprise.yaml new file mode 100644 index 0000000..0602e21 --- /dev/null +++ b/examples/wpa_enterprise.yaml @@ -0,0 +1,26 @@ +network: + version: 2 + wifis: + wl0: + access-points: + workplace: + auth: + key-management: eap + method: ttls + anonymous-identity: "@internal.example.com" + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + university: + auth: + key-management: eap + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + open-network: + auth: + key-management: none + dhcp4: yes diff --git a/netplan.completions b/netplan.completions new file mode 100644 index 0000000..0561128 --- /dev/null +++ b/netplan.completions @@ -0,0 +1,45 @@ +# netplan(1) completion -*- shell-script -*- + +_compgen_help() +{ + local options=$1 + shift + + compgen -W '${options} help' $@ +} + +_netplan() +{ + local cur prev words cword + _init_completion || return + + case $prev in + netplan) + COMPREPLY=( $( _compgen_help 'apply generate ifupdown-migrate ip' "$cur" ) ) + return + ;; + apply|generate) + return + ;; + ifupdown-migrate) + return + ;; + ip) + COMPREPLY=( $( _compgen_help 'leases' -- "$cur" ) ) + return + ;; + leases) + if [ "${COMP_WORDS[COMP_CWORD-2]}" = "ip" ]; then + _available_interfaces -a + fi + return + ;; + esac + + if [[ $cur == -* ]]; then + COMPREPLY=( $( compgen -W '$( _parse_help "$1" )' -- "$cur" ) ) + fi +} && +complete -F _netplan netplan + +# ex: ts=4 sw=4 et filetype=sh diff --git a/netplan/__init__.py b/netplan/__init__.py new file mode 100644 index 0000000..6e4e922 --- /dev/null +++ b/netplan/__init__.py @@ -0,0 +1,20 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from netplan.cli.core import Netplan + +__all__ = [Netplan] diff --git a/netplan/cli/__init__.py b/netplan/cli/__init__.py new file mode 100644 index 0000000..7f084b2 --- /dev/null +++ b/netplan/cli/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/netplan/cli/commands/__init__.py b/netplan/cli/commands/__init__.py new file mode 100644 index 0000000..0a5a229 --- /dev/null +++ b/netplan/cli/commands/__init__.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from netplan.cli.commands.apply import NetplanApply +from netplan.cli.commands.generate import NetplanGenerate +from netplan.cli.commands.ip import NetplanIp +from netplan.cli.commands.migrate import NetplanMigrate +from netplan.cli.commands.try_command import NetplanTry +from netplan.cli.commands.info import NetplanInfo +from netplan.cli.commands.set import NetplanSet +from netplan.cli.commands.get import NetplanGet + +__all__ = [ + 'NetplanApply', + 'NetplanGenerate', + 'NetplanIp', + 'NetplanMigrate', + 'NetplanTry', + 'NetplanInfo', + 'NetplanSet', + 'NetplanGet', +] diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py new file mode 100644 index 0000000..b1d4b9c --- /dev/null +++ b/netplan/cli/commands/apply.py @@ -0,0 +1,334 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018-2020 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Łukasz 'sil2100' Zemczak +# Author: Lukas 'slyon' Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan apply command line''' + +import logging +import os +import sys +import glob +import subprocess +import shutil +import netifaces + +import netplan.cli.utils as utils +from netplan.configmanager import ConfigManager, ConfigurationError +from netplan.cli.sriov import apply_sriov_config +from netplan.cli.ovs import apply_ovs_cleanup + + +OVS_CLEANUP_SERVICE = 'netplan-ovs-cleanup.service' + + +class NetplanApply(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='apply', + description='Apply current netplan config to running system', + leaf=True) + self.sriov_only = False + self.only_ovs_cleanup = False + + def run(self): # pragma: nocover (covered in autopkgtest) + self.parser.add_argument('--sriov-only', action='store_true', + help='Only apply SR-IOV related configuration and exit') + self.parser.add_argument('--only-ovs-cleanup', action='store_true', + help='Only clean up old OpenVSwitch interfaces and exit') + + self.func = self.command_apply + + self.parse_args() + self.run_command() + + 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. + # https://github.com/snapcore/snapd/pull/5915 + 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") + else: + raise RuntimeError( + "failed to communicate with dbus service: error %s" % res) + 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) + + @staticmethod + def is_composite_member(composites, phy): + """ + Is this physical interface a member of a 'composite' virtual + interface? (bond, bridge) + """ + for composite in composites: + for _, settings in composite.items(): + if not type(settings) is dict: + continue + members = settings.get('interfaces', []) + for iface in members: + if iface == phy: + return True + + return False + + @staticmethod + def process_link_changes(interfaces, config_manager): # pragma: nocover (covered in autopkgtest) + """ + Go through the pending changes and pick what needs special handling. + Only applies to non-critical interfaces which can be safely updated. + """ + + changes = {} + phys = dict(config_manager.physical_interfaces) + composite_interfaces = [config_manager.bridges, config_manager.bonds] + + # Find physical interfaces which need a rename + # But do not rename virtual interfaces + for phy, settings in phys.items(): + if not settings or not isinstance(settings, dict): + continue # Skip special values, like "renderer: ..." + newname = settings.get('set-name') + if not newname: + continue # Skip if no new name needs to be set + match = settings.get('match') + if not match: + continue # Skip if no match for current name is given + if NetplanApply.is_composite_member(composite_interfaces, phy): + logging.debug('Skipping composite member {}'.format(phy)) + # do not rename members of virtual devices. MAC addresses + # may be the same for all interface members. + continue + # Find current name of the interface, according to match conditions and globs (name, mac, driver) + current_iface_name = utils.find_matching_iface(interfaces, match) + if not current_iface_name: + logging.warning('Cannot find unique matching interface for {}: {}'.format(phy, match)) + continue + if current_iface_name == newname: + # Skip interface if it already has the correct name + logging.debug('Skipping correctly named interface: {}'.format(newname)) + continue + if settings.get('critical', False): + # Skip interfaces defined as critical, as we should not take them down in order to rename + logging.warning('Cannot rename {} ({} -> {}) at runtime (needs reboot), due to being critical' + .format(phy, current_iface_name, newname)) + continue + + # record the interface rename change + changes[current_iface_name] = {'name': newname} + + logging.debug('Link changes: {}'.format(changes)) + return changes + + @staticmethod + def process_sriov_config(config_manager, exit_on_error=True): # pragma: nocover (covered in autopkgtest) + try: + apply_sriov_config(config_manager) + except (ConfigurationError, RuntimeError) as e: + logging.error(str(e)) + if exit_on_error: + sys.exit(1) + + @staticmethod + def process_ovs_cleanup(config_manager, ovs_old, ovs_current, exit_on_error=True): # pragma: nocover (autopkgtest) + try: + apply_ovs_cleanup(config_manager, ovs_old, ovs_current) + except (OSError, RuntimeError) as e: + logging.error(str(e)) + if exit_on_error: + sys.exit(1) diff --git a/netplan/cli/commands/generate.py b/netplan/cli/commands/generate.py new file mode 100644 index 0000000..4900d8f --- /dev/null +++ b/netplan/cli/commands/generate.py @@ -0,0 +1,85 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan generate command line''' + +import logging +import os +import sys +import subprocess +import shutil + +import netplan.cli.utils as utils + + +class NetplanGenerate(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='generate', + description='Generate backend specific configuration files' + ' from /etc/netplan/*.yaml', + leaf=True) + + def run(self): + self.parser.add_argument('--root-dir', + help='Search for and generate configuration files in this root directory instead of /') + self.parser.add_argument('--mapping', + help='Display the netplan device ID/backend/interface name mapping and exit.') + + self.func = self.command_generate + + self.parse_args() + self.run_command() + + def command_generate(self): + # 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") # pragma: nocover + # 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. + # https://github.com/snapcore/snapd/pull/10212 + res = subprocess.call([busctl, "call", "--quiet", "--system", + "io.netplan.Netplan", # the service + "/io/netplan/Netplan", # the object + "io.netplan.Netplan", # the interface + "Generate", # the method + ]) + + if res != 0: + if res == 130: + raise PermissionError( + "failed to communicate with dbus service") + else: + raise RuntimeError( + "failed to communicate with dbus service: error %s" % res) + else: + return + + argv = [utils.get_generator_path()] + if self.root_dir: + argv += ['--root-dir', self.root_dir] + if self.mapping: + argv += ['--mapping', self.mapping] + logging.debug('command generate: running %s', argv) + # FIXME: os.execv(argv[0], argv) would be better but fails coverage + sys.exit(subprocess.call(argv)) diff --git a/netplan/cli/commands/get.py b/netplan/cli/commands/get.py new file mode 100644 index 0000000..85fd2f6 --- /dev/null +++ b/netplan/cli/commands/get.py @@ -0,0 +1,67 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan get command line''' + +import yaml +import re + +import netplan.cli.utils as utils +from netplan.configmanager import ConfigManager + + +class NetplanGet(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='get', + description='Get a setting by specifying a nested key like "ethernets.eth0.addresses", or "all"', + leaf=True) + + def run(self): + self.parser.add_argument('key', type=str, nargs='?', default='all', help='The nested key in dotted format') + self.parser.add_argument('--root-dir', default='/', + help='Read configuration files from this root directory instead of /') + + self.func = self.command_get + + self.parse_args() + self.run_command() + + 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'(? +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan info command line''' + +import netplan.cli.utils as utils +import netplan._features + + +class NetplanInfo(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='info', + description='Show available features', + leaf=True) + + def run(self): # pragma: nocover (covered in autopkgtest) + format_group = self.parser.add_mutually_exclusive_group(required=False) + format_group.add_argument('--json', dest='version_format', action='store_const', + const='json', + help='Output version and features in JSON format') + format_group.add_argument('--yaml', dest='version_format', action='store_const', + const='yaml', + help='Output version and features in YAML format') + + self.func = self.command_info + self.parse_args() + self.run_command() + + def command_info(self): + + netplan_version = { + 'netplan.io': { + 'website': 'https://netplan.io/', + } + } + + flags = netplan._features.NETPLAN_FEATURE_FLAGS + netplan_version['netplan.io'].update({'features': flags}) + + # Default to output in YAML format. + if self.version_format is None: + self.version_format = 'yaml' + + if self.version_format == 'json': + import json + print(json.dumps(netplan_version, indent=2)) + + elif self.version_format == 'yaml': + import yaml + print(yaml.dump(netplan_version, indent=2, default_flow_style=False)) diff --git a/netplan/cli/commands/ip.py b/netplan/cli/commands/ip.py new file mode 100644 index 0000000..b7a7f29 --- /dev/null +++ b/netplan/cli/commands/ip.py @@ -0,0 +1,153 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan ip command line''' + +import logging +import os +import sys +import subprocess +from subprocess import CalledProcessError + +import netplan.cli.utils as utils + +lease_path = { + 'networkd': { + 'pattern': 'run/systemd/netif/leases/{lease_id}', + 'method': 'ifindex', + }, + 'NetworkManager': { + 'pattern': 'var/lib/NetworkManager/dhclient-{lease_id}-{interface}.lease', + 'method': 'nm_connection', + }, +} + + +class NetplanIp(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='ip', + description='Retrieve IP information from the system', + leaf=False) + + def run(self): + self.command_leases = NetplanIpLeases() + + # subcommand: leases + p_ip_leases = self.subparsers.add_parser('leases', + help='Display IP leases', + add_help=False) + p_ip_leases.set_defaults(func=self.command_leases.run, commandclass=self.command_leases) + + self.parse_args() + self.run_command() + + +class NetplanIpLeases(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='ip leases', + description='Display IP leases', + leaf=True) + + def run(self): + self.parser.add_argument('interface', + help='Interface for which to display IP lease settings.') + self.parser.add_argument('--root-dir', + help='Search for configuration files in this root directory instead of /') + + self.func = self.command_ip_leases + + self.parse_args() + self.run_command() + + def command_ip_leases(self): + + if self.interface == 'help': # pragma: nocover (covered in autopkgtest) + self.print_usage() + + def find_lease_file(mapping): + def lease_method_ifindex(): + ifindex_f = os.path.join('/sys/class/net', self.interface, 'ifindex') + try: + with open(ifindex_f) as f: + return f.readlines()[0].strip() + except Exception as e: + logging.debug('Cannot read file %s: %s', ifindex_f, str(e)) + raise + + def lease_method_nm_connection(): # pragma: nocover (covered in autopkgtest) + # FIXME: handle older versions of NM where 'nmcli dev show' doesn't exist + try: + nmcli_dev_out = subprocess.Popen(['nmcli', 'dev', 'show', self.interface], + env={'LC_ALL': 'C'}, + stdout=subprocess.PIPE) + for line in nmcli_dev_out.stdout: + line = line.decode('utf-8') + if 'GENERAL.CONNECTION' in line: + conn_id = line.split(':')[1].rstrip().strip() + nmcli_con_out = subprocess.Popen(['nmcli', 'con', 'show', 'id', conn_id], + env={'LC_ALL': 'C'}, + stdout=subprocess.PIPE) + for line in nmcli_con_out.stdout: + line = line.decode('utf-8') + if 'connection.uuid' in line: + return line.split(':')[1].rstrip().strip() + except Exception as e: + raise Exception('Could not find a NetworkManager connection for the interface: %s' % str(e)) + raise Exception('Could not find a NetworkManager connection for the interface') + + lease_pattern = lease_path[mapping['backend']]['pattern'] + lease_method = lease_path[mapping['backend']]['method'] + + try: + lease_id = eval("lease_method_" + lease_method)() + + # We found something to build the path to the lease file with, + # at this point we may have something to look at; but if not, + # we'll rely on open() throwing an error. + # This might happen if networkd doesn't use DHCP for the interface, + # for instance. + with open(os.path.join('/', + os.path.abspath(self.root_dir) if self.root_dir else "", + lease_pattern.format(interface=self.interface, + lease_id=lease_id))) as f: + for line in f.readlines(): + print(line.rstrip()) + except Exception as e: + print("No lease found for interface '%s': %s" % (self.interface, str(e)), + file=sys.stderr) + sys.exit(1) + + argv = [utils.get_generator_path()] + if self.root_dir: + argv += ['--root-dir', self.root_dir] + argv += ['--mapping', self.interface] + + # Extract out of the generator our mapping in a dict. + logging.debug('command ip leases: running %s', argv) + try: + out = subprocess.check_output(argv, universal_newlines=True) + except CalledProcessError: # pragma: nocover (better be covered in autopkgtest) + sys.exit(1) + mapping = {} + mapping_s = out.split(',') + for keyvalue in mapping_s: + key, value = keyvalue.strip().split('=') + mapping[key] = value + + find_lease_file(mapping) diff --git a/netplan/cli/commands/migrate.py b/netplan/cli/commands/migrate.py new file mode 100644 index 0000000..7133b11 --- /dev/null +++ b/netplan/cli/commands/migrate.py @@ -0,0 +1,416 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan migrate command line''' + +import logging +import os +import sys +import re +from glob import glob +import yaml +from collections import OrderedDict +import ipaddress + +import netplan.cli.utils as utils + + +class NetplanMigrate(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='migrate', + description='Migration of /etc/network/interfaces to netplan', + leaf=True, + testing=True) + + def parse_dns_options(self, if_options, if_config): + """Parse dns options (dns-nameservers and dns-search) from if_options + (an interface options dict) into the interface configuration if_config + Mutates the arguments in place. + """ + if 'dns-nameservers' in if_options: + if 'nameservers' not in if_config: + if_config['nameservers'] = {} + if 'addresses' not in if_config['nameservers']: + if_config['nameservers']['addresses'] = [] + for ns in if_options['dns-nameservers'].split(' '): + # allow multiple spaces in the dns-nameservers entry + if not ns: + continue + # validate? + if_config['nameservers']['addresses'] += [ns] + del if_options['dns-nameservers'] + if 'dns-search' in if_options: + if 'nameservers' not in if_config: + if_config['nameservers'] = {} + if 'search' not in if_config['nameservers']: + if_config['nameservers']['search'] = [] + for domain in if_options['dns-search'].split(' '): + # allow multiple spaces in the dns-search entry + if not domain: + continue + if_config['nameservers']['search'] += [domain] + del if_options['dns-search'] + + def parse_mtu(self, iface, if_options, if_config): + """Parse out the MTU. Operates the same way as parse_dns_options + iface is the name of the interface, used only to print error messages + """ + + if 'mtu' in if_options: + try: + mtu = int(if_options['mtu']) + except ValueError: + logging.error('%s: cannot parse "%s" as an MTU', iface, if_options['mtu']) + sys.exit(2) + + if 'mtu' in if_config and not if_config['mtu'] == mtu: + logging.error('%s: tried to set MTU=%d, but already have MTU=%d', iface, mtu, if_config['mtu']) + sys.exit(2) + + if_config['mtu'] = mtu + del if_options['mtu'] + + def parse_hwaddress(self, iface, if_options, if_config): + """Parse out the manually configured MAC. + Operates the same way as parse_dns_options + iface is the name of the interface, used only to print error messages + """ + + if 'hwaddress' in if_options: + if 'macaddress' in if_config and not if_config['macaddress'] == if_options['hwaddress']: + logging.error('%s: tried to set MAC %s, but already have MAC %s', iface, + if_options['hwaddress'], if_config['macaddress']) + sys.exit(2) + + if_config['macaddress'] = if_options['hwaddress'] + del if_options['hwaddress'] + + def run(self): + self.parser.add_argument('--root-dir', + help='Search for and generate configuration files in this root directory instead of /') + self.parser.add_argument('--dry-run', action='store_true', + help='Print converted netplan configuration to stdout instead of writing/changing files') + self.func = self.command_migrate + + self.parse_args() + self.run_command() + + def command_migrate(self): + netplan_config = {} + try: + ifaces, auto_ifaces = self.parse_ifupdown(self.root_dir or '') + except ValueError as e: + logging.error(str(e)) + sys.exit(2) + for iface, family_config in ifaces.items(): + for family, config in family_config.items(): + logging.debug('Converting %s family %s %s', iface, family, config) + if iface not in auto_ifaces: + logging.error('%s: non-automatic interfaces are not supported', iface) + sys.exit(2) + if config['method'] == 'loopback': + # both systemd and modern ifupdown set up lo automatically + logging.debug('Ignoring loopback interface %s', iface) + elif config['method'] == 'dhcp': + c = netplan_config.setdefault('network', {}).setdefault('ethernets', {}).setdefault(iface, {}) + + self.parse_dns_options(config['options'], c) + self.parse_hwaddress(iface, config['options'], c) + + if config['options']: + logging.error('%s: option(s) %s are not supported for dhcp method', + iface, ", ".join(config['options'].keys())) + sys.exit(2) + if family == 'inet': + c['dhcp4'] = True + else: + assert family == 'inet6' + c['dhcp6'] = True + + elif config['method'] == 'static': + c = netplan_config.setdefault('network', {}).setdefault('ethernets', {}).setdefault(iface, {}) + + if 'addresses' not in c: + c['addresses'] = [] + + self.parse_dns_options(config['options'], c) + self.parse_mtu(iface, config['options'], c) + self.parse_hwaddress(iface, config['options'], c) + + # ipv4 + if family == 'inet': + # Already handled: mtu, hwaddress + # Supported: address netmask gateway + # Not supported yet: metric(?) + # No YAML support: pointopoint scope broadcast + supported_opts = set(['address', 'netmask', 'gateway']) + unsupported_opts = set(['broadcast', 'metric', 'pointopoint', 'scope']) + + opts = set(config['options'].keys()) + bad_opts = opts - supported_opts + if bad_opts: + for unsupported in bad_opts.intersection(unsupported_opts): + logging.error('%s: unsupported %s option "%s"', iface, family, unsupported) + sys.exit(2) + for unknown in bad_opts - unsupported_opts: + logging.error('%s: unknown %s option "%s"', iface, family, unknown) + sys.exit(2) + + # the address may contain a /prefix suffix, or + # the netmask property may be used. It's not clear + # what happens if both are supplied. + if 'address' not in config['options']: + logging.error('%s: no address supplied in static method', iface) + sys.exit(2) + + if '/' in config['options']['address']: + addr_spec = config['options']['address'].split('/')[0] + net_spec = config['options']['address'] + else: + if 'netmask' not in config['options']: + logging.error('%s: address does not specify prefix length, and netmask not specified', + iface) + sys.exit(2) + addr_spec = config['options']['address'] + net_spec = config['options']['address'] + '/' + config['options']['netmask'] + + try: + ipaddr = ipaddress.IPv4Address(addr_spec) + except ipaddress.AddressValueError as a: + logging.error('%s: error parsing "%s" as an IPv4 address: %s', iface, addr_spec, a) + sys.exit(2) + + try: + ipnet = ipaddress.IPv4Network(net_spec, strict=False) + except ipaddress.NetmaskValueError as a: + logging.error('%s: error parsing "%s" as an IPv4 network: %s', iface, net_spec, a) + sys.exit(2) + + c['addresses'] += [str(ipaddr) + '/' + str(ipnet.prefixlen)] + + if 'gateway' in config['options']: + # validate? + c['gateway4'] = config['options']['gateway'] + + # ipv6 + else: + assert family == 'inet6' + + # Already handled: mtu, hwaddress + # supported: address netmask gateway + # partially supported: accept_ra (0/1 supported, 2 has no YAML rep) + # unsupported: metric(?) + # no YAML representation: media autoconf privext scope + # preferred-lifetime dad-attempts dad-interval + supported_opts = set(['address', 'netmask', 'gateway', 'accept_ra']) + unsupported_opts = set(['metric', 'media', 'autoconf', 'privext', + 'scope', 'preferred-lifetime', 'dad-attempts', 'dad-interval']) + + opts = set(config['options'].keys()) + bad_opts = opts - supported_opts + if bad_opts: + for unsupported in bad_opts.intersection(unsupported_opts): + logging.error('%s: unsupported %s option "%s"', iface, family, unsupported) + sys.exit(2) + for unknown in bad_opts - unsupported_opts: + logging.error('%s: unknown %s option "%s"', iface, family, unknown) + sys.exit(2) + + # the address may contain a /prefix suffix, or + # the netmask property may be used. It's not clear + # what happens if both are supplied. + if 'address' not in config['options']: + logging.error('%s: no address supplied in static method', iface) + sys.exit(2) + + if '/' in config['options']['address']: + addr_spec = config['options']['address'].split('/')[0] + net_spec = config['options']['address'] + else: + if 'netmask' not in config['options']: + logging.error('%s: address does not specify prefix length, and netmask not specified', + iface) + sys.exit(2) + addr_spec = config['options']['address'] + net_spec = config['options']['address'] + '/' + config['options']['netmask'] + + try: + ipaddr = ipaddress.IPv6Address(addr_spec) + except ipaddress.AddressValueError as a: + logging.error('%s: error parsing "%s" as an IPv6 address: %s', iface, addr_spec, a) + sys.exit(2) + + try: + ipnet = ipaddress.IPv6Network(net_spec, strict=False) + except ipaddress.NetmaskValueError as a: + logging.error('%s: error parsing "%s" as an IPv6 network: %s', iface, net_spec, a) + sys.exit(2) + + c['addresses'] += [str(ipaddr) + '/' + str(ipnet.prefixlen)] + + if 'gateway' in config['options']: + # validate? + c['gateway6'] = config['options']['gateway'] + + if 'accept_ra' in config['options']: + if config['options']['accept_ra'] == '0': + c['accept_ra'] = False + elif config['options']['accept_ra'] == '1': + c['accept_ra'] = True + elif config['options']['accept_ra'] == '2': + logging.error('%s: netplan does not support accept_ra=2', iface) + sys.exit(2) + else: + logging.error('%s: unexpected accept_ra value "%s"', iface, + config['options']['accept_ra']) + sys.exit(2) + + else: # pragma nocover + # this should be unreachable + logging.error('%s: method %s is not supported', iface, config['method']) + sys.exit(2) + + if_config = os.path.join(self.root_dir or '/', 'etc/network/interfaces') + + if netplan_config: + netplan_config['network']['version'] = 2 + netplan_yaml = yaml.dump(netplan_config) + if self.dry_run: + print(netplan_yaml) + else: + dest = os.path.join(self.root_dir or '/', 'etc/netplan/10-ifupdown.yaml') + try: + os.makedirs(os.path.dirname(dest)) + except FileExistsError: + pass + try: + with open(dest, 'x') as f: + f.write(netplan_yaml) + except FileExistsError: + logging.error('%s already exists; remove it if you want to run the migration again', dest) + sys.exit(3) + logging.info('migration complete, wrote %s', dest) + else: + logging.info('ifupdown does not configure any interfaces, nothing to migrate') + + if not self.dry_run: + logging.info('renaming %s to %s.netplan-converted', if_config, if_config) + os.rename(if_config, if_config + '.netplan-converted') + + def _ifupdown_lines_from_file(self, rootdir, path): + '''Return normalized lines from ifupdown config + + This resolves "source" and "source-directory" includes. + ''' + def expand_source_arg(rootdir, curdir, line): + arg = line.split()[1] + if arg.startswith('/'): + return rootdir + arg + else: + return curdir + '/' + arg + + lines = [] + rootdir_len = len(rootdir) + 1 + try: + with open(rootdir + '/' + path) as f: + logging.debug('reading %s', f.name) + for line in f: + # normalize, strip empty lines and comments + line = line.strip() + if not line or line.startswith('#'): + continue + + if line.startswith('source-directory '): + valid_re = re.compile('^[a-zA-Z0-9_-]+$') + d = expand_source_arg(rootdir, os.path.dirname(f.name), line) + for f in os.listdir(d): + if valid_re.match(f): + lines += self._ifupdown_lines_from_file(rootdir, os.path.join(d[rootdir_len:], f)) + elif line.startswith('source '): + for f in glob(expand_source_arg(rootdir, os.path.dirname(f.name), line)): + lines += self._ifupdown_lines_from_file(rootdir, f[rootdir_len:]) + else: + lines.append(line) + except FileNotFoundError: + logging.debug('%s/%s does not exist, ignoring', rootdir, path) + return lines + + def parse_ifupdown(self, rootdir='/'): + '''Parse ifupdown configuration. + + Return (iface_name → family → {method, options}, auto_ifaces: set) tuple + on successful parsing, or a ValueError when encountering an invalid file or + ifupdown features which are not supported (such as "mapping"). + + options is itself a dictionary option_name → value. + ''' + # expected number of fields for every possible keyword, excluding the keyword itself + fieldlen = {'auto': 1, 'allow-auto': 1, 'allow-hotplug': 1, 'mapping': 1, 'no-scripts': 1, 'iface': 3} + + # read and normalize all lines from config, with resolving includes + lines = self._ifupdown_lines_from_file(rootdir, '/etc/network/interfaces') + + ifaces = OrderedDict() + auto = set() + in_options = None # interface name if parsing options lines after iface stanza + in_family = None + + # we now have resolved all includes and normalized lines + for line in lines: + fields = line.split() + + try: + # does the line start with a known stanza field? + exp_len = fieldlen[fields[0]] + logging.debug('line fields %s (expected length: %i)', fields, exp_len) + in_options = None # stop option line parsing of iface stanza + in_family = None + except KeyError: + # no known stanza field, are we in an iface stanza and parsing options? + if in_options: + logging.debug('in_options %s, parsing as option: %s', in_options, line) + ifaces[in_options][in_family]['options'][fields[0]] = line.split(maxsplit=1)[1] + continue + else: + raise ValueError('Unknown stanza type %s' % fields[0]) + + # do we have the expected #parameters? + if len(fields) != exp_len + 1: + raise ValueError('Expected %i fields for stanza type %s but got %i' % + (exp_len, fields[0], len(fields) - 1)) + + # we have a valid stanza line now, handle them + if fields[0] in ('auto', 'allow-auto', 'allow-hotplug'): + auto.add(fields[1]) + elif fields[0] == 'mapping': + raise ValueError('mapping stanza is not supported') + elif fields[0] == 'no-scripts': + pass # ignore these + elif fields[0] == 'iface': + if fields[2] not in ('inet', 'inet6'): + raise ValueError('Unknown address family %s' % fields[2]) + if fields[3] not in ('loopback', 'static', 'dhcp'): + raise ValueError('Unsupported method %s' % fields[3]) + in_options = fields[1] + in_family = fields[2] + ifaces.setdefault(fields[1], OrderedDict())[in_family] = {'method': fields[3], 'options': {}} + else: + raise NotImplementedError('stanza type %s is not implemented' % fields[0]) # pragma nocover + + logging.debug('final parsed interfaces: %s; auto ifaces: %s', ifaces, auto) + return (ifaces, auto) diff --git a/netplan/cli/commands/set.py b/netplan/cli/commands/set.py new file mode 100644 index 0000000..3bf7dc6 --- /dev/null +++ b/netplan/cli/commands/set.py @@ -0,0 +1,169 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan set command line''' + +import os +import yaml +import tempfile +import re +import logging +import shutil + +import netplan.cli.utils as utils +from netplan.configmanager import ConfigManager + +FALLBACK_HINT = '70-netplan-set' +GLOBAL_KEYS = ['renderer', 'version'] + + +class NetplanSet(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='set', + description='Add new setting by specifying a dotted key=value pair like ethernets.eth0.dhcp4=true', + leaf=True) + + def run(self): + self.parser.add_argument('key_value', type=str, + help='The nested key=value pair in dotted format. Value can be NULL to delete a key.') + self.parser.add_argument('--origin-hint', type=str, + help='Can be used to help choose a name for the overwrite YAML file. \ + A .yaml suffix will be appended automatically.') + self.parser.add_argument('--root-dir', default='/', + help='Overwrite configuration files in this root directory instead of /') + + self.func = self.command_set + + self.parse_args() + self.run_command() + + def split_tree_by_hint(self, set_tree) -> (str, dict): + network = set_tree.get('network', {}) + # A mapping of 'origin-hint' -> YAML tree (one subtree per netdef) + subtrees = dict() + for devtype in network: + if devtype in GLOBAL_KEYS: + continue # special handling of global keys down below + for netdef in network.get(devtype, []): + hint = FALLBACK_HINT + filename = utils.netplan_get_filename_by_id(netdef, self.root_dir) + if filename: + hint = os.path.basename(filename)[:-5] # strip prefix and .yaml + netdef_tree = {'network': {devtype: {netdef: network.get(devtype).get(netdef)}}} + # Merge all netdef trees which are going to be written to the same file/hint + subtrees[hint] = self.merge(subtrees.get(hint, {}), netdef_tree) + + # Merge GLOBAL_KEYS into one of the available subtrees + # Write to same file (if only one hint/subtree is available) + # Write to FALLBACK_HINT if multiple hints/subtrees are available, as we do not know where it is supposed to go + if any(network.get(key) for key in GLOBAL_KEYS): + # Write to the same file, if we have only one file-hint or to FALLBACK_HINT otherwise + hint = list(subtrees)[0] if len(subtrees) == 1 else FALLBACK_HINT + for key in GLOBAL_KEYS: + tree = {'network': {key: network.get(key)}} + subtrees[hint] = self.merge(subtrees.get(hint, {}), tree) + + # return a list of (str:hint, dict:subtree) tuples + return subtrees.items() + + def command_set(self): + if self.origin_hint is not None and len(self.origin_hint) == 0: + raise Exception('Invalid/empty origin-hint') + split = self.key_value.split('=', 1) + if len(split) != 2: + raise Exception('Invalid value specified') + key, value = split + set_tree = self.parse_key(key, yaml.safe_load(value)) + + hints = [(self.origin_hint, set_tree)] + # Override YAML config in each individual netdef file if origin-hint is not set + if self.origin_hint is None: + hints = self.split_tree_by_hint(set_tree) + + for hint, subtree in hints: + self.write_file(subtree, hint + '.yaml', self.root_dir) + + def parse_key(self, key, value): + # The 'network.' prefix is optional for netsted keys, its always assumed to be there + if not key.startswith('network.') and not key == 'network': + key = 'network.' + key + # Split at '.' but not at '\.' via negative lookbehind expression + split = re.split(r'(? +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan try command line''' + +import os +import time +import signal +import sys +import logging +import subprocess + +from netplan.configmanager import ConfigManager +import netplan.cli.utils as utils +from netplan.cli.commands.apply import NetplanApply +import netplan.terminal + +# Keep a timeout long enough to allow the network to converge, 60 seconds may +# be slightly short given some complex configs, i.e. if STP must reconverge. +DEFAULT_INPUT_TIMEOUT = 120 + + +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 = None + self.t_settings = None + self.t = None + + @property + def config_manager(self): # pragma: nocover (called by later commands) + if not self._config_manager: + self._config_manager = ConfigManager() + return self._config_manager + + 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() + self.t = netplan.terminal.Terminal(fd) + self.t.save(self.t_settings) + + # we really don't want to be interrupted while doing backup/revert operations + signal.signal(signal.SIGINT, self._signal_handler) + signal.signal(signal.SIGUSR1, self._signal_handler) + + self.backup() + self.setup() + + NetplanApply().command_apply(run_generate=True, sync=True, exit_on_error=False) + + self.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 occurred: %s" % e) + print("\nReverting.") + self.revert() + finally: + if self.t: + self.t.reset(self.t_settings) + 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, sig, frame): # pragma: nocover (requires user input) + if sig == signal.SIGUSR1: + raise netplan.terminal.InputAccepted() + else: + if self.configuration_changed: + raise netplan.terminal.InputRejected() diff --git a/netplan/cli/core.py b/netplan/cli/core.py new file mode 100644 index 0000000..3d6c392 --- /dev/null +++ b/netplan/cli/core.py @@ -0,0 +1,50 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Martin Pitt +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan command line''' + +import logging +import os + +import netplan.cli.utils as utils + + +class Netplan(utils.NetplanCommand): + + def __init__(self): + super().__init__(command_id='', + description='Network configuration in YAML', + leaf=False) + + def parse_args(self): + import netplan.cli.commands + + self._import_subcommands(netplan.cli.commands) + + super().parse_args() + + def main(self): + self.parse_args() + + if self.debug: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') + os.environ['G_MESSAGES_DEBUG'] = 'all' + else: + logging.basicConfig(level=logging.INFO, format='%(message)s') + + self.run_command() diff --git a/netplan/cli/ovs.py b/netplan/cli/ovs.py new file mode 100644 index 0000000..d8466fc --- /dev/null +++ b/netplan/cli/ovs.py @@ -0,0 +1,178 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Lukas 'slyon' Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import os +import subprocess +import re + +OPENVSWITCH_OVS_VSCTL = '/usr/bin/ovs-vsctl' +# Defaults for non-optional settings, as defined here: +# http://www.openvswitch.org/ovs-vswitchd.conf.db.5.pdf +DEFAULTS = { + # Mandatory columns: + 'mcast_snooping_enable': 'false', + 'rstp_enable': 'false', +} +GLOBALS = { + # Global commands: + 'set-ssl': ('del-ssl', 'get-ssl'), + 'set-fail-mode': ('del-fail-mode', 'get-fail-mode'), + 'set-controller': ('del-controller', 'get-controller'), +} + + +def _del_col(type, iface, column, value): + """Cleanup values from a column (i.e. "column=value")""" + default = DEFAULTS.get(column) + if default is None: + # removes the exact value only if it was set by netplan + subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, value]) + elif default and default != value: + # reset to default, if its not the default already + subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'set', type, iface, '%s=%s' % (column, default)]) + + +def _del_dict(type, iface, column, key, value): + """Cleanup values from a dictionary (i.e. "column:key=value")""" + # removes the exact value only if it was set by netplan + subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, column, key, _escape_colon(value)]) + + +# for ovsdb remove: column key's value can not contain bare ':', need to escape with '\' +def _escape_colon(literal): + return re.sub(r'([^\\]):', r'\g<1>\:', literal) + + +def _del_global(type, iface, key, value): + """Cleanup commands from the global namespace""" + del_cmd, get_cmd = GLOBALS.get(key, (None, None)) + if del_cmd == 'del-ssl': + iface = None + + if del_cmd: + args_get = [OPENVSWITCH_OVS_VSCTL, get_cmd] + args_del = [OPENVSWITCH_OVS_VSCTL, del_cmd] + if iface: + args_get.append(iface) + args_del.append(iface) + # Check the current value of a global command and compare it to the tag-value, e.g.: + # * get-ssl: netplan/global/set-ssl=/private/key.pem,/another/cert.pem,/some/ca-cert.pem + # Private key: /private/key.pem + # Certificate: /another/cert.pem + # CA Certificate: /some/ca-cert.pem + # Bootstrap: false + # * get-fail-mode: netplan/global/set-fail-mode=secure + # secure + # * get-controller: netplan/global/set-controller=tcp:127.0.0.1:1337,unix:/some/socket + # tcp:127.0.0.1:1337 + # unix:/some/socket + out = subprocess.check_output(args_get, universal_newlines=True) + # Clean it only if the exact same value(s) were set by netplan. + # Don't touch it if other values were set by another integration. + if all(item in out for item in value.split(',')): + subprocess.check_call(args_del) + else: + raise Exception('Reset command unkown for:', key) + + +def clear_setting(type, iface, setting, value): + """Check if this setting is in a dict or a colum and delete accordingly""" + split = setting.split('/', 2) + col = split[1] + if col == 'global' and len(split) > 2: + _del_global(type, iface, split[2], value) + elif len(split) > 2: + _del_dict(type, iface, split[1], split[2], value) + else: + _del_col(type, iface, split[1], value) + # Cleanup the tag itself (i.e. "netplan/column[/key]") + subprocess.check_call([OPENVSWITCH_OVS_VSCTL, 'remove', type, iface, 'external-ids', setting]) + + +def is_ovs_interface(iface, interfaces): + assert isinstance(interfaces, dict) + if not isinstance(interfaces.get(iface), dict): + logging.debug('Ignoring special key: {} ({})'.format(iface, interfaces.get(iface))) + return False + elif interfaces.get(iface, {}).get('openvswitch') is not None: + return True + else: + return any(is_ovs_interface(i, interfaces) for i in interfaces.get(iface, {}).get('interfaces', [])) + + +def apply_ovs_cleanup(config_manager, ovs_old, ovs_current): # pragma: nocover (covered in autopkgtest) + """ + Query OpenVSwitch state through 'ovs-vsctl' and filter for netplan=true + tagged ports/bonds and bridges. Delete interfaces which are not defined + in the current configuration. + Also filter for individual settings tagged netplan/[/ 0: + subprocess.check_call([OPENVSWITCH_OVS_VSCTL, '--if-exists', 'del-bond-iface', iface]) + else: + subprocess.check_call([OPENVSWITCH_OVS_VSCTL, '--if-exists', t[1], iface]) + + # Step 2: Clean up the settings of the remaining interfaces + for t in ('Port', 'Bridge', 'Interface', 'Open_vSwitch', 'Controller'): + cols = 'name,external-ids' + if t == 'Open_vSwitch': + cols = 'external-ids' + elif t == 'Controller': + cols = '_uuid,external-ids' # handle _uuid as if it would be the iface 'name' + out = subprocess.check_output([OPENVSWITCH_OVS_VSCTL, '--columns=%s' % cols, + '-f', 'csv', '-d', 'bare', '--no-headings', 'list', t], + universal_newlines=True) + for line in out.splitlines(): + if 'netplan/' in line: + iface = '.' + extids = line + if t != 'Open_vSwitch': + iface, extids = line.split(',', 1) + # Check each line (interface) if it contains any netplan tagged settings, e.g.: + # ovs0,"iface-id=myhostname netplan=true netplan/external-ids/iface-id=myhostname" + # ovs1,"netplan=true netplan/global/set-fail-mode=standalone netplan/mcast_snooping_enable=false" + for entry in extids.strip('"').split(' '): + if entry.startswith('netplan/') and '=' in entry: + setting, val = entry.split('=', 1) + clear_setting(t, iface, setting, val) + + # Show the warning only if we are or have been working with OVS definitions + elif ovs_old or ovs_current: + logging.warning('ovs-vsctl is missing, cannot tear down old OpenVSwitch interfaces') diff --git a/netplan/cli/sriov.py b/netplan/cli/sriov.py new file mode 100644 index 0000000..43e0259 --- /dev/null +++ b/netplan/cli/sriov.py @@ -0,0 +1,334 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Łukasz 'sil2100' Zemczak +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import os +import subprocess + +from collections import defaultdict + +import netplan.cli.utils as utils +from netplan.configmanager import ConfigurationError + +import netifaces + + +def _get_target_interface(interfaces, config_manager, pf_link, pfs): + if pf_link not in pfs: + # handle the match: syntax, get the actual device name + pf_dev = config_manager.ethernets[pf_link] + pf_match = pf_dev.get('match') + if pf_match: + # now here it's a bit tricky + set_name = pf_dev.get('set-name') + if set_name and set_name in interfaces: + # if we had a match: stanza and set-name: this means we should + # assume that, if found, the interface has already been + # renamed - use the new name + pfs[pf_link] = set_name + else: + # no set-name (or interfaces not yet renamed) so we need to do + # the matching ourselves + by_name = pf_match.get('name') + by_mac = pf_match.get('macaddress') + by_driver = pf_match.get('driver') + + for interface in interfaces: + if ((by_name and not utils.is_interface_matching_name(interface, by_name)) or + (by_mac and not utils.is_interface_matching_macaddress(interface, by_mac)) or + (by_driver and not utils.is_interface_matching_driver_name(interface, by_driver))): + continue + # we have a matching PF + # store the matching interface in the dictionary of + # active PFs, but error out if we matched more than one + if pf_link in pfs: + raise ConfigurationError('matched more than one interface for a PF device: %s' % pf_link) + pfs[pf_link] = interface + else: + # no match field, assume entry name is the interface name + if pf_link in interfaces: + pfs[pf_link] = pf_link + + return pfs.get(pf_link, None) + + +def get_vf_count_and_functions(interfaces, config_manager, + vf_counts, vfs, pfs): + """ + Go through the list of netplan ethernet devices and identify which are + PFs and VFs, matching the former with actual networking interfaces. + Count how many VFs each PF will need. + """ + explicit_counts = {} + for ethernet, settings in config_manager.ethernets.items(): + if not settings: + continue + if ethernet == 'renderer': + continue + + # we now also support explicitly stating how many VFs should be + # allocated for a PF + explicit_num = settings.get('virtual-function-count') + if explicit_num: + pf = _get_target_interface(interfaces, config_manager, ethernet, pfs) + if pf: + explicit_counts[pf] = explicit_num + continue + + pf_link = settings.get('link') + if pf_link and pf_link in config_manager.ethernets: + _get_target_interface(interfaces, config_manager, pf_link, pfs) + + if pf_link in pfs: + vf_counts[pfs[pf_link]] += 1 + else: + logging.warning('could not match physical interface for the defined PF: %s' % pf_link) + # continue looking for other VFs + continue + + # we can't yet perform matching on VFs as those are only + # created later - but store, for convenience, all the valid + # VFs that we encounter so far + vfs[ethernet] = None + + # sanity check: since we can explicitly state the VF count, make sure + # that this number isn't smaller than the actual number of VFs declared + # the explicit number also overrides the number of actual VFs + for pf, count in explicit_counts.items(): + if pf in vf_counts and vf_counts[pf] > count: + raise ConfigurationError( + 'more VFs allocated than the explicit size declared: %s > %s' % (vf_counts[pf], count)) + vf_counts[pf] = count + + +def set_numvfs_for_pf(pf, vf_count): + """ + Allocate the required number of VFs for the selected PF. + """ + if vf_count > 256: + raise ConfigurationError( + 'cannot allocate more VFs for PF %s than the SR-IOV maximum: %s > 256' % (pf, vf_count)) + + devdir = os.path.join('/sys/class/net', pf, 'device') + numvfs_path = os.path.join(devdir, 'sriov_numvfs') + totalvfs_path = os.path.join(devdir, 'sriov_totalvfs') + try: + with open(totalvfs_path) as f: + vf_max = int(f.read().strip()) + except IOError as e: + raise RuntimeError('failed parsing sriov_totalvfs for %s: %s' % (pf, str(e))) + except ValueError: + raise RuntimeError('invalid sriov_totalvfs value for %s' % pf) + + if vf_count > vf_max: + raise ConfigurationError( + 'cannot allocate more VFs for PF %s than supported: %s > %s (sriov_totalvfs)' % (pf, vf_count, vf_max)) + + try: + with open(numvfs_path, 'w') as f: + f.write(str(vf_count)) + except IOError as e: + bail = True + if e.errno == 16: # device or resource busy + logging.warning('device or resource busy while setting sriov_numvfs for %s, trying workaround' % pf) + try: + # doing this in two open/close sequences so that + # it's as close to writing via shell as possible + with open(numvfs_path, 'w') as f: + f.write('0') + with open(numvfs_path, 'w') as f: + f.write(str(vf_count)) + except IOError as e_inner: + e = e_inner + else: + bail = False + if bail: + raise RuntimeError('failed setting sriov_numvfs to %s for %s: %s' % (vf_count, pf, str(e))) + + return True + + +def perform_hardware_specific_quirks(pf): + """ + Perform any hardware-specific quirks for the given SR-IOV device to make + sure all the VF-count changes are applied. + """ + devdir = os.path.join('/sys/class/net', pf, 'device') + try: + with open(os.path.join(devdir, 'vendor')) as f: + device_id = f.read().strip()[2:] + with open(os.path.join(devdir, 'device')) as f: + vendor_id = f.read().strip()[2:] + except IOError as e: + raise RuntimeError('could not determine vendor and device ID of %s: %s' % (pf, str(e))) + + combined_id = ':'.join([vendor_id, device_id]) + quirk_devices = () # TODO: add entries to the list + if combined_id in quirk_devices: + # some devices need special handling, so this is the place + + # Currently this part is empty, but has been added as a preemptive + # measure, as apparently a lot of SR-IOV cards have issues with + # dynamically allocating VFs. Some cards seem to require a full + # kernel module reload cycle after changing the sriov_numvfs value + # for the changes to come into effect. + # Any identified card/vendor can then be special-cased here, if + # needed. + pass + + +def apply_vlan_filter_for_vf(pf, vf, vlan_name, vlan_id, prefix='/'): + """ + Apply the hardware VLAN filtering for the selected VF. + """ + + # this is more complicated, because to do this, we actually need to have + # the vf index - just knowing the vf interface name is not enough + vf_index = None + # the prefix argument is here only for unit testing purposes + vf_devdir = os.path.join(prefix, 'sys/class/net', vf, 'device') + vf_dev_id = os.path.basename(os.readlink(vf_devdir)) + pf_devdir = os.path.join(prefix, 'sys/class/net', pf, 'device') + for f in os.listdir(pf_devdir): + if 'virtfn' in f: + dev_path = os.path.join(pf_devdir, f) + dev_id = os.path.basename(os.readlink(dev_path)) + if dev_id == vf_dev_id: + vf_index = f[6:] + break + + if not vf_index: + raise RuntimeError( + 'could not determine the VF index for %s while configuring vlan %s' % (vf, vlan_name)) + + # now, create the VLAN filter + # TODO: would be best if we did this directl via python, without calling + # the iproute tooling + try: + subprocess.check_call(['ip', 'link', 'set', + 'dev', pf, + 'vf', vf_index, + 'vlan', str(vlan_id)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + raise RuntimeError( + 'failed setting SR-IOV VLAN filter for vlan %s (ip link set command failed)' % vlan_name) + + +def apply_sriov_config(config_manager): + """ + Go through all interfaces, identify which ones are SR-IOV VFs, create + them and perform all other necessary setup. + """ + config_manager.parse() + interfaces = netifaces.interfaces() + + # for sr-iov devices, we identify VFs by them having a link: field + # pointing to an PF. So let's browse through all ethernet devices, + # find all that are VFs and count how many of those are linked to + # particular PFs, as we need to then set the numvfs for each. + vf_counts = defaultdict(int) + # we also store all matches between VF/PF netplan entry names and + # interface that they're currently matching to + vfs = {} + pfs = {} + + get_vf_count_and_functions( + interfaces, config_manager, vf_counts, vfs, pfs) + + # setup the required number of VFs per PF + # at the same time store which PFs got changed in case the NICs + # require some special quirks for the VF number to change + vf_count_changed = [] + if vf_counts: + for pf, vf_count in vf_counts.items(): + if not set_numvfs_for_pf(pf, vf_count): + continue + + vf_count_changed.append(pf) + + if vf_count_changed: + # some cards need special treatment when we want to change the + # number of enabled VFs + for pf in vf_count_changed: + perform_hardware_specific_quirks(pf) + + # also, since the VF number changed, the interfaces list also + # changed, so we need to refresh it + interfaces = netifaces.interfaces() + + # now in theory we should have all the new VFs set up and existing; + # this is needed because we will have to now match the defined VF + # entries to existing interfaces, otherwise we won't be able to set + # filtered VLANs for those. + # XXX: does matching those even make sense? + for vf in vfs: + settings = config_manager.ethernets.get(vf) + match = settings.get('match') + if match: + # right now we only match by name, as I don't think matching per + # driver and/or macaddress makes sense + by_name = match.get('name') + # by_mac = match.get('macaddress') + # by_driver = match.get('driver') + # TODO: print warning if other matches are provided + + for interface in interfaces: + if by_name and not utils.is_interface_matching_name(interface, by_name): + continue + if vf in vfs and vfs[vf]: + raise ConfigurationError('matched more than one interface for a VF device: %s' % vf) + vfs[vf] = interface + else: + if vf in interfaces: + vfs[vf] = vf + + filtered_vlans_set = set() + for vlan, settings in config_manager.vlans.items(): + # there is a special sriov vlan renderer that one can use to mark + # a selected vlan to be done in hardware (VLAN filtering) + if settings.get('renderer') == 'sriov': + # this only works for SR-IOV VF interfaces + link = settings.get('link') + vlan_id = settings.get('id') + if not vlan_id: + raise ConfigurationError( + 'no id property defined for SR-IOV vlan %s' % vlan) + + vf = vfs.get(link) + if not vf: + # it is possible this is not an error, for instance when + # the configuration has been defined 'for the future' + # XXX: but maybe we should error out here as well? + logging.warning( + 'SR-IOV vlan defined for %s but link %s is either not a VF or has no matches' % (vlan, link)) + continue + + # get the parent pf interface + # first we fetch the related vf netplan entry + vf_parent_entry = config_manager.ethernets.get(link).get('link') + # and finally, get the matched pf interface + pf = pfs.get(vf_parent_entry) + + if vf in filtered_vlans_set: + raise ConfigurationError( + 'interface %s for netplan device %s (%s) already has an SR-IOV vlan defined' % (vf, link, vlan)) + + # TODO: make sure that we don't apply the filter twice + apply_vlan_filter_for_vf(pf, vf, vlan, vlan_id) + filtered_vlans_set.add(vf) diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py new file mode 100644 index 0000000..0a04692 --- /dev/null +++ b/netplan/cli/utils.py @@ -0,0 +1,297 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018-2020 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Łukasz 'sil2100' Zemczak +# Author: Lukas 'slyon' Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import os +import logging +import fnmatch +import argparse +import subprocess +import netifaces +import re +import ctypes +import ctypes.util + +NM_SERVICE_NAME = 'NetworkManager.service' +NM_SNAP_SERVICE_NAME = 'snap.network-manager.networkmanager.service' + + +class _GError(ctypes.Structure): + _fields_ = [("domain", ctypes.c_uint32), ("code", ctypes.c_int), ("message", ctypes.c_char_p)] + + +lib = ctypes.CDLL(ctypes.util.find_library('netplan')) +lib.netplan_parse_yaml.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.POINTER(_GError))] +lib.netplan_get_filename_by_id.restype = ctypes.c_char_p + + +def netplan_parse(path): + # Clear old NetplanNetDefinitions from libnetplan memory + lib.netplan_clear_netdefs() + err = ctypes.POINTER(_GError)() + ret = bool(lib.netplan_parse_yaml(path.encode(), ctypes.byref(err))) + if not ret: + raise Exception(err.contents.message.decode('utf-8')) + lib.netplan_finish_parse(ctypes.byref(err)) + if err: + raise Exception(err.contents.message.decode('utf-8')) + return True + + +def netplan_get_filename_by_id(netdef_id, rootdir): + res = lib.netplan_get_filename_by_id(netdef_id.encode(), rootdir.encode()) + return res.decode('utf-8') if res else None + + +def get_generator_path(): + return os.environ.get('NETPLAN_GENERATE_PATH', '/lib/netplan/generate') + + +def is_nm_snap_enabled(): + return subprocess.call(['systemctl', '--quiet', 'is-enabled', NM_SNAP_SERVICE_NAME], stderr=subprocess.DEVNULL) == 0 + + +def nmcli(args): # pragma: nocover (covered in autopkgtest) + binary_name = 'nmcli' + + if is_nm_snap_enabled(): + binary_name = 'network-manager.nmcli' + + subprocess.check_call([binary_name] + args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def nm_running(): # pragma: nocover (covered in autopkgtest) + '''Check if NetworkManager is running''' + + try: + nmcli(['general']) + return True + except (OSError, subprocess.SubprocessError): + return False + + +def nm_interfaces(paths, devices): + pat = re.compile('^interface-name=(.*)$') + interfaces = set() + for path in paths: + with open(path, 'r') as f: + for line in f: + m = pat.match(line) + if m: + # Expand/match globbing of interface names, to real devices + interfaces.update(set(fnmatch.filter(devices, m.group(1)))) + break # skip to next file + return interfaces + + +def systemctl_network_manager(action, sync=False): + # If the network-manager snap is installed use its service + # name rather than the one of the deb packaged NetworkManager + if is_nm_snap_enabled(): + return systemctl(action, [NM_SNAP_SERVICE_NAME], sync) + return systemctl(action, [NM_SERVICE_NAME], sync) # pragma: nocover (covered in autopkgtest) + + +def systemctl(action, services, sync=False): + if len(services) >= 1: + command = ['systemctl', action] + + if not sync: + command.append('--no-block') + + command.extend(services) + + subprocess.check_call(command) + + +def networkd_interfaces(): + interfaces = set() + out = subprocess.check_output(['networkctl', '--no-pager', '--no-legend'], universal_newlines=True) + for line in out.splitlines(): + s = line.strip().split(' ') + if s[0].isnumeric() and s[-1] not in ['unmanaged', 'linger']: + interfaces.add(s[1]) + return interfaces + + +def networkctl_reconfigure(interfaces): + subprocess.check_call(['networkctl', 'reload']) + if len(interfaces) >= 1: + subprocess.check_call(['networkctl', 'reconfigure'] + list(interfaces)) + + +def systemctl_is_active(unit_pattern): + '''Return True if at least one matching unit is running''' + if subprocess.call(['systemctl', '--quiet', 'is-active', unit_pattern]) == 0: + return True + return False + + +def systemctl_daemon_reload(): + '''Reload systemd unit files from disk and re-calculate its dependencies''' + subprocess.check_call(['systemctl', 'daemon-reload']) + + +def ip_addr_flush(iface): + '''Flush all IP addresses of a given interface via iproute2''' + subprocess.check_call(['ip', 'addr', 'flush', iface], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + +def get_interface_driver_name(interface, only_down=False): # pragma: nocover (covered in autopkgtest) + devdir = os.path.join('/sys/class/net', interface) + if only_down: + try: + with open(os.path.join(devdir, 'operstate')) as f: + state = f.read().strip() + if state != 'down': + logging.debug('device %s operstate is %s, not changing', interface, state) + return None + except IOError as e: + logging.error('Cannot determine operstate of %s: %s', interface, str(e)) + return None + + try: + driver = os.path.realpath(os.path.join(devdir, 'device', 'driver')) + driver_name = os.path.basename(driver) + except IOError as e: + logging.debug('Cannot replug %s: cannot read link %s/device: %s', interface, devdir, str(e)) + return None + + return driver_name + + +def get_interface_macaddress(interface): + # return an empty list (and string) if no LL data can be found + link = netifaces.ifaddresses(interface).get(netifaces.AF_LINK, [{}])[0] + return link.get('addr', '') + + +def is_interface_matching_name(interface, match_name): + # globs are supported + return fnmatch.fnmatchcase(interface, match_name) + + +def is_interface_matching_driver_name(interface, match_driver): + driver_name = get_interface_driver_name(interface) + # globs are supported + return fnmatch.fnmatchcase(driver_name, match_driver) + + +def is_interface_matching_macaddress(interface, match_mac): + macaddress = get_interface_macaddress(interface) + # exact, case insensitive match. globs are not supported + return match_mac.lower() == macaddress.lower() + + +def find_matching_iface(interfaces, match): + assert isinstance(match, dict) + + # Filter for match.name glob, fallback to '*' + name_glob = match.get('name') if match.get('name', False) else '*' + matches = fnmatch.filter(interfaces, name_glob) + + # Filter for match.macaddress (exact match) + if len(matches) > 1 and match.get('macaddress'): + matches = list(filter(lambda iface: is_interface_matching_macaddress(iface, match.get('macaddress')), matches)) + + # Filter for match.driver glob + if len(matches) > 1 and match.get('driver'): + matches = list(filter(lambda iface: is_interface_matching_driver_name(iface, match.get('driver')), matches)) + + # Return current name of unique matched interface, if available + if len(matches) != 1: + logging.info(matches) + return None + return matches[0] + + +class NetplanCommand(argparse.Namespace): + + def __init__(self, command_id, description, leaf=True, testing=False): + self.command_id = command_id + self.description = description + self.leaf_command = leaf + self.testing = testing + self._args = None + self.debug = False + self.commandclass = None + self.subcommands = {} + self.subcommand = None + self.func = None + + self.parser = argparse.ArgumentParser(prog="%s %s" % (sys.argv[0], command_id), + description=description, + add_help=True) + self.parser.add_argument('--debug', action='store_true', + help='Enable debug messages') + if not leaf: + self.subparsers = self.parser.add_subparsers(title='Available commands', + metavar='', dest='subcommand') + p_help = self.subparsers.add_parser('help', + description='Show this help message', + help='Show this help message') + p_help.set_defaults(func=self.print_usage) + + def update(self, args): + self._args = args + + def parse_args(self): + ns, self._args = self.parser.parse_known_args(args=self._args, namespace=self) + + if not self.subcommand and not self.leaf_command: + print('You need to specify a command', file=sys.stderr) + self.print_usage() + + def run_command(self): + if self.commandclass: + self.commandclass.update(self._args) + + # TODO: (cyphermox) this is actually testable in tests/cli.py; add it. + if self.leaf_command and 'help' in self._args: # pragma: nocover (covered in autopkgtest) + self.print_usage() + + self.func() + + def print_usage(self): + self.parser.print_help(file=sys.stderr) + sys.exit(os.EX_USAGE) + + def _add_subparser_from_class(self, name, commandclass): + instance = commandclass() + + self.subcommands[name] = {} + self.subcommands[name]['class'] = name + self.subcommands[name]['instance'] = instance + + if instance.testing: + if not os.environ.get('ENABLE_TEST_COMMANDS', None): + return + + p = self.subparsers.add_parser(instance.command_id, + description=instance.description, + help=instance.description, + add_help=False) + p.set_defaults(func=instance.run, commandclass=instance) + self.subcommands[name]['parser'] = p + + def _import_subcommands(self, submodules): + import inspect + for name, obj in inspect.getmembers(submodules): + if inspect.isclass(obj) and issubclass(obj, NetplanCommand): + self._add_subparser_from_class(name, obj) diff --git a/netplan/configmanager.py b/netplan/configmanager.py new file mode 100644 index 0000000..9278d04 --- /dev/null +++ b/netplan/configmanager.py @@ -0,0 +1,320 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan configuration manager''' + +import glob +import logging +import os +import shutil +import sys +import tempfile +import yaml + + +class ConfigManager(object): + + def __init__(self, prefix="/", extra_files={}): + self.prefix = prefix + self.tempdir = tempfile.mkdtemp(prefix='netplan_') + self.temp_etc = os.path.join(self.tempdir, "etc") + self.temp_run = os.path.join(self.tempdir, "run") + self.extra_files = extra_files + self.config = {} + self.new_interfaces = set() + + @property + def network(self): + return self.config['network'] + + @property + def interfaces(self): + interfaces = {} + interfaces.update(self.ovs_ports) + interfaces.update(self.ethernets) + interfaces.update(self.modems) + interfaces.update(self.wifis) + interfaces.update(self.bridges) + interfaces.update(self.bonds) + interfaces.update(self.tunnels) + interfaces.update(self.vlans) + return interfaces + + @property + def physical_interfaces(self): + interfaces = {} + interfaces.update(self.ethernets) + interfaces.update(self.modems) + interfaces.update(self.wifis) + return interfaces + + @property + def ovs_ports(self): + return self.network['ovs_ports'] + + @property + def openvswitch(self): + return self.network['openvswitch'] + + @property + def ethernets(self): + return self.network['ethernets'] + + @property + def modems(self): + return self.network['modems'] + + @property + def wifis(self): + return self.network['wifis'] + + @property + def bridges(self): + return self.network['bridges'] + + @property + def bonds(self): + return self.network['bonds'] + + @property + def tunnels(self): + return self.network['tunnels'] + + @property + def vlans(self): + return self.network['vlans'] + + @property + def nm_devices(self): + return self.network['nm-devices'] + + @property + def version(self): + return self.network['version'] + + @property + def renderer(self): + return self.network['renderer'] + + @property + def tree(self): + return self.strip_tree(self.config) + + @staticmethod + def strip_tree(data): + '''clear empty branches''' + new_data = {} + for k, v in data.items(): + if isinstance(v, dict): + v = ConfigManager.strip_tree(v) + if v not in (u'', None, {}): + new_data[k] = v + return new_data + + def parse(self, extra_config=[]): + """ + Parse all our config files to return an object that describes the system's + entire configuration, so that it can later be interrogated. + + Returns a dict that contains the entire, collated and merged YAML. + """ + # TODO: Clean this up, there's no solid reason why we should parse YAML + # in two different spots; here and in parse.c. We'd do better by + # parsing things once, in C form, and having some small glue + # Cpython code to call on the right methods and return an object + # that is meaningful for the Python code; but minimal parsing in + # pure Python will do for now. ~cyphermox + + # /run/netplan shadows /etc/netplan/, which shadows /lib/netplan + names_to_paths = {} + for yaml_dir in ['lib', 'etc', 'run']: + for yaml_file in glob.glob(os.path.join(self.prefix, yaml_dir, 'netplan', '*.yaml')): + names_to_paths[os.path.basename(yaml_file)] = yaml_file + + files = [names_to_paths[name] for name in sorted(names_to_paths.keys())] + + self.config['network'] = { + 'ovs_ports': {}, + 'openvswitch': {}, + 'ethernets': {}, + 'modems': {}, + 'wifis': {}, + 'bridges': {}, + 'bonds': {}, + 'tunnels': {}, + 'vlans': {}, + 'nm-devices': {}, + 'version': None, + 'renderer': None + } + for yaml_file in files: + self._merge_yaml_config(yaml_file) + + for yaml_file in extra_config: + self.new_interfaces |= self._merge_yaml_config(yaml_file) + + logging.debug("Merged config:\n{}".format(yaml.dump(self.tree, default_flow_style=False))) + + def add(self, config_dict): + for config_file in config_dict: + self._copy_file(config_file, config_dict[config_file]) + self.extra_files.update(config_dict) + + def backup(self, backup_config_dir=True): + if backup_config_dir: + self._copy_tree(os.path.join(self.prefix, "etc/netplan"), + os.path.join(self.temp_etc, "netplan")) + self._copy_tree(os.path.join(self.prefix, "run/NetworkManager/system-connections"), + os.path.join(self.temp_run, "NetworkManager", "system-connections"), + missing_ok=True) + self._copy_tree(os.path.join(self.prefix, "run/systemd/network"), + os.path.join(self.temp_run, "systemd", "network"), + missing_ok=True) + + def revert(self): + try: + for extra_file in dict(self.extra_files): + os.unlink(self.extra_files[extra_file]) + del self.extra_files[extra_file] + temp_nm_path = "{}/NetworkManager/system-connections".format(self.temp_run) + temp_networkd_path = "{}/systemd/network".format(self.temp_run) + if os.path.exists(temp_nm_path): + shutil.rmtree(os.path.join(self.prefix, "run/NetworkManager/system-connections")) + self._copy_tree(temp_nm_path, + os.path.join(self.prefix, "run/NetworkManager/system-connections")) + if os.path.exists(temp_networkd_path): + shutil.rmtree(os.path.join(self.prefix, "run/systemd/network")) + self._copy_tree(temp_networkd_path, + os.path.join(self.prefix, "run/systemd/network")) + except Exception as e: # pragma: nocover (only relevant to filesystem failures) + # If we reach here, we're in big trouble. We may have wiped out + # file NM or networkd are using, and we most likely removed the + # "new" config -- or at least our copy of it. + # Given that we're in some halfway done revert; warn the user + # aggressively and drop everything; leaving any remaining backups + # around for the user to handle themselves. + logging.error("Something really bad happened while reverting config: {}".format(e)) + logging.error("You should verify the netplan YAML in /etc/netplan and probably run 'netplan apply' again.") + sys.exit(-1) + + def cleanup(self): + shutil.rmtree(self.tempdir) + + def _copy_file(self, src, dst): + shutil.copy(src, dst) + + def _copy_tree(self, src, dst, missing_ok=False): + try: + shutil.copytree(src, dst) + except FileNotFoundError: + if missing_ok: + pass + else: + raise + + def _merge_ovs_ports_config(self, orig, new): + new_interfaces = set() + ports = dict() + if 'ports' in new: + for p1, p2 in new.get('ports'): + # Spoof an interface config for patch ports, which are usually + # just strings. Add 'peer' and mark it via 'openvswitch' key. + ports[p1] = {'peer': p2, 'openvswitch': {}} + ports[p2] = {'peer': p1, 'openvswitch': {}} + changed_ifaces = list(ports.keys()) + + for ifname in changed_ifaces: + iface = ports.pop(ifname) + if ifname in orig: + logging.debug("{} exists in {}".format(ifname, orig)) + orig[ifname].update(iface) + else: + logging.debug("{} not found in {}".format(ifname, orig)) + orig[ifname] = iface + new_interfaces.add(ifname) + + return new_interfaces + + def _merge_interface_config(self, orig, new): + new_interfaces = set() + changed_ifaces = list(new.keys()) + + for ifname in changed_ifaces: + iface = new.pop(ifname) + if ifname in orig: + logging.debug("{} exists in {}".format(ifname, orig)) + orig[ifname].update(iface) + else: + logging.debug("{} not found in {}".format(ifname, orig)) + orig[ifname] = iface + new_interfaces.add(ifname) + + return new_interfaces + + def _merge_yaml_config(self, yaml_file): + new_interfaces = set() + + try: + with open(yaml_file) as f: + yaml_data = yaml.load(f, Loader=yaml.CSafeLoader) + network = None + if yaml_data is not None: + network = yaml_data.get('network') + if network: + if 'openvswitch' in network: + new = self._merge_ovs_ports_config(self.ovs_ports, network.get('openvswitch')) + new_interfaces |= new + self.network['openvswitch'] = network.get('openvswitch') + if 'ethernets' in network: + new = self._merge_interface_config(self.ethernets, network.get('ethernets')) + new_interfaces |= new + if 'modems' in network: + new = self._merge_interface_config(self.modems, network.get('modems')) + new_interfaces |= new + if 'wifis' in network: + new = self._merge_interface_config(self.wifis, network.get('wifis')) + new_interfaces |= new + if 'bridges' in network: + new = self._merge_interface_config(self.bridges, network.get('bridges')) + new_interfaces |= new + if 'bonds' in network: + new = self._merge_interface_config(self.bonds, network.get('bonds')) + new_interfaces |= new + if 'tunnels' in network: + new = self._merge_interface_config(self.tunnels, network.get('tunnels')) + new_interfaces |= new + if 'vlans' in network: + new = self._merge_interface_config(self.vlans, network.get('vlans')) + new_interfaces |= new + if 'nm-devices' in network: + new = self._merge_interface_config(self.nm_devices, network.get('nm-devices')) + new_interfaces |= new + if 'version' in network: + self.network['version'] = network.get('version') + if 'renderer' in network: + self.network['renderer'] = network.get('renderer') + return new_interfaces + except (IOError, yaml.YAMLError): # pragma: nocover (filesystem failures/invalid YAML) + logging.error('Error while loading {}, aborting.'.format(yaml_file)) + sys.exit(1) + + +class ConfigurationError(Exception): + """ + Configuration could not be parsed or has otherwise failed to apply + """ + pass diff --git a/netplan/terminal.py b/netplan/terminal.py new file mode 100644 index 0000000..2dd5967 --- /dev/null +++ b/netplan/terminal.py @@ -0,0 +1,157 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Terminal / input handling +""" + +import fcntl +import os +import termios +import select +import sys + + +class Terminal(object): + """ + Do minimal terminal mangling to prompt users for input + """ + + def __init__(self, fd): + self.fd = fd + self.orig_flags = None + self.orig_term = None + self.save() + + def enable_echo(self): + if sys.stdin.isatty(): + attrs = termios.tcgetattr(self.fd) + attrs[3] = attrs[3] | termios.ICANON + attrs[3] = attrs[3] | termios.ECHO + termios.tcsetattr(self.fd, termios.TCSANOW, attrs) + + def disable_echo(self): + if sys.stdin.isatty(): + attrs = termios.tcgetattr(self.fd) + attrs[3] = attrs[3] & ~termios.ICANON + attrs[3] = attrs[3] & ~termios.ECHO + termios.tcsetattr(self.fd, termios.TCSANOW, attrs) + + def enable_nonblocking_io(self): + flags = fcntl.fcntl(self.fd, fcntl.F_GETFL) + fcntl.fcntl(self.fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + def disable_nonblocking_io(self): + flags = fcntl.fcntl(self.fd, fcntl.F_GETFL) + fcntl.fcntl(self.fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK) + + def get_confirmation_input(self, timeout=120, message=None): # pragma: nocover (requires user input) + """ + Get a "confirmation" input from the user, for at most (timeout) + seconds. Optionally, customize the message to be displayed. + + timeout -- timeout to wait for input (default 120) + message -- optional customized message ("Press ENTER to (message)") + + raises: + InputAccepted -- the user confirmed the changes + InputRejected -- the user rejected the changes + """ + print("Do you want to keep these settings?\n\n") + + settings = dict() + self.save(settings) + self.disable_echo() + self.enable_nonblocking_io() + + if not message: + message = "accept the new configuration" + + print("Press ENTER before the timeout to {}\n\n".format(message)) + timeout_now = timeout + while (timeout_now > 0): + print("Changes will revert in {:>{}} seconds".format(timeout_now, len(str(timeout))), end='\r') + + # wait at most 1 second for usable input from stdin + select.select([sys.stdin], [], [], 1) + try: + # retrieve any input from the terminal. select() either has + # timed out with no input, or found something we can retrieve. + c = sys.stdin.read() + if (c == '\n'): + self.reset(settings) + # Yay, user has accepted the changes! + raise InputAccepted() + except TypeError: + # read() above is non-blocking, if there is nothing to read it + # will return TypeError, which we should ignore -- on to the + # next iteration until timeout. + pass + timeout_now -= 1 + + # We reached the timeout for our loop, now revert our change for + # non-blocking I/O and signal the caller the changes were essentially + # rejected. + self.reset(settings) + raise InputRejected() + + def save(self, dest=None): + """ + Save the terminal's current attributes and flags + + Optional argument: + - dest: if set, save settings to this dict + """ + orig_flags = fcntl.fcntl(self.fd, fcntl.F_GETFL) + orig_term = None + if sys.stdin.isatty(): + orig_term = termios.tcgetattr(self.fd) + if dest is not None: + dest.update({'flags': orig_flags, + 'term': orig_term}) + else: + self.orig_flags = orig_flags + self.orig_term = orig_term + + def reset(self, orig=None): + """ + Reset the terminal to its original attributes and flags + + Optional argument: + - orig: if set, reset to settings from this dict + """ + orig_term = None + orig_flags = None + if orig is not None: + orig_term = orig.get('term') + orig_flags = orig.get('flags') + else: + orig_term = self.orig_term + orig_flags = self.orig_flags + if sys.stdin.isatty(): + termios.tcsetattr(self.fd, termios.TCSAFLUSH, orig_term) + fcntl.fcntl(self.fd, fcntl.F_SETFL, orig_flags) + + +class InputAccepted(Exception): + """ Denotes has accepted input""" + pass + + +class InputRejected(Exception): + """ Denotes that the user has rejected input""" + pass diff --git a/rpm/netplan.spec b/rpm/netplan.spec new file mode 100644 index 0000000..c3edab1 --- /dev/null +++ b/rpm/netplan.spec @@ -0,0 +1,131 @@ +# Ensure hardened build on EL7 +%global _hardened_build 1 + +# Ubuntu calls their own software netplan.io in the archive due to name conflicts +%global ubuntu_name netplan.io + +# If the definition isn't available for python3_pkgversion, define it +%{?!python3_pkgversion:%global python3_pkgversion 3} + +# If this isn't defined, define it +%{?!_systemdgeneratordir:%global _systemdgeneratordir /usr/lib/systemd/system-generators} + +# Force auto-byte-compilation to Python 3 +%global __python %{__python3} + + +Name: netplan +Version: 0.95 +Release: 0%{?dist} +Summary: Network configuration tool using YAML +Group: System Environment/Base +License: GPLv3 +URL: http://netplan.io/ +Source0: https://github.com/canonical/%{name}/archive/%{version}/%{version}.tar.gz + +BuildRequires: gcc +BuildRequires: make +BuildRequires: pkgconfig(bash-completion) +BuildRequires: pkgconfig(systemd) +BuildRequires: pkgconfig(glib-2.0) +BuildRequires: pkgconfig(yaml-0.1) +BuildRequires: pkgconfig(uuid) +BuildRequires: %{_bindir}/pandoc +BuildRequires: python%{python3_pkgversion}-devel +# For tests +BuildRequires: %{_sbindir}/ip +BuildRequires: python%{python3_pkgversion}-coverage +BuildRequires: python%{python3_pkgversion}-netifaces +BuildRequires: python%{python3_pkgversion}-nose +BuildRequires: python%{python3_pkgversion}-pycodestyle +BuildRequires: python%{python3_pkgversion}-pyflakes +BuildRequires: python%{python3_pkgversion}-PyYAML + +# /usr/sbin/netplan is a Python 3 script that requires netifaces and PyYAML +Requires: python%{python3_pkgversion}-netifaces +Requires: python%{python3_pkgversion}-PyYAML +# 'ip' command is used in netplan apply subcommand +Requires: %{_sbindir}/ip + +# netplan supports either systemd or NetworkManager as backends to configure the network +Requires: systemd + +%if 0%{?el7} +# systemd-networkd is a separate subpackage in EL7 +Requires: systemd-networkd +%endif + +%if 0%{?fedora} || 0%{?rhel} >= 8 +# NetworkManager is preferred, but wpa_supplicant can be used directly for Wi-Fi networks +Suggests: (NetworkManager or wpa_supplicant) +%endif + +# Provide the package name that Ubuntu uses for it too... +Provides: %{ubuntu_name} = %{version}-%{release} +Provides: %{ubuntu_name}%{?_isa} = %{version}-%{release} + +%description +netplan reads network configuration from /etc/netplan/*.yaml which are written by administrators, +installers, cloud image instantiations, or other OS deployments. During early boot, it generates +backend specific configuration files in /run to hand off control of devices to a particular +networking daemon. + +Currently supported backends are systemd-networkd and NetworkManager. + + +%prep +%autosetup -p1 + +# Drop -Werror to avoid the following error: +# /usr/include/glib-2.0/glib/glib-autocleanups.h:28:3: error: 'ip_str' may be used uninitialized in this function [-Werror=maybe-uninitialized] +sed -e "s/-Werror//g" -i Makefile + + +%build +%make_build CFLAGS="%{optflags}" + + +%install +%make_install ROOTPREFIX=%{_prefix} + +# Pre-create the config directory +mkdir -p %{buildroot}%{_sysconfdir}/%{name} + + +%check +make check + + +%files +%license COPYING +%doc debian/changelog +%doc %{_docdir}/%{name}/ +%{_sbindir}/%{name} +%{_datadir}/%{name}/ +%{_unitdir}/%{name}*.service +%{_systemdgeneratordir}/%{name} +%{_mandir}/man5/%{name}.5* +%{_mandir}/man8/%{name}*.8* +%dir %{_sysconfdir}/%{name} +%{_prefix}/lib/%{name}/ +%{_datadir}/bash-completion/completions/%{name} + + +%changelog +* Fri Dec 14 2018 Mathieu Trudel-Lapierre - 0.95 +- Update to 0.95 + +* Sat Oct 13 2018 Neal Gompa - 0.40.3-0 +- Rebase to 0.40.3 + +* Tue Mar 13 2018 Neal Gompa - 0.34-0.1 +- Update to 0.34 + +* Wed Mar 7 2018 Neal Gompa - 0.33-0.1 +- Rebase to 0.33 + +* Sat Nov 4 2017 Neal Gompa - 0.30-1 +- Rebase to 0.30 + +* Sun Jul 2 2017 Neal Gompa - 0.23~17.04.1-1 +- Initial packaging diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..c3f5ae6 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,42 @@ +name: netplan +version: git +summary: Backend-agnostic network configuration in YAML +description: | + Netplan is a utility for easily configuring networking on a linux system. + You simply create a YAML description of the required network interfaces and + what each should be configured to do. From this description Netplan will + generate all the necessary configuration for your chosen renderer tool. +grade: devel +confinement: classic + +apps: + netplan: + command: usr/sbin/netplan + environment: + PYTHONPATH: $PYTHONPATH:$SNAP/usr/lib/python3/dist-packages + +parts: + netplan: + source: https://github.com/canonical/netplan.git + plugin: make + build-packages: + - bash-completion + - libglib2.0-dev + - libyaml-dev + - uuid-dev + - pandoc + - pkg-config + - python3 + - python3-coverage + - python3-yaml + - python3-netifaces + - python3-nose + - pyflakes3 + - pep8 + - systemd + stage-packages: + - iproute2 + - python3 + - python3-netifaces + - python3-yaml + - systemd diff --git a/src/dbus.c b/src/dbus.c new file mode 100644 index 0000000..f0aa53a --- /dev/null +++ b/src/dbus.c @@ -0,0 +1,798 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "_features.h" +#include "util.h" + +typedef struct { + sd_bus_slot *slot; + gboolean invalidated; +} NetplanConfigData; + +typedef struct { + sd_bus *bus; + sd_event_source *try_es; + GPid try_pid; /* semaphore. There can only be one 'netplan try' child process at a time */ + const char *config_id; /* current config ID, during any io.netplan.Netplan.Config calls */ + char *handler_id; /* copy of pending config ID, during io.netplan.Netplan.Config.Try() */ + char *config_dirty; /* Currently pending Set() config object id */ + GHashTable *config_data; /* data of to the /io/netplan/Netplan/config/ objects */ +} NetplanData; + +static const char* NETPLAN_SUBDIRS[3] = {"etc", "run", "lib"}; +static const char* NETPLAN_GLOBAL_CONFIG = "BACKUP"; +static char* NETPLAN_ROOT = "/"; /* Can be modified for testing netplan-dbus */ + +static void +invalidate_other_config(gpointer key, gpointer value, gpointer user_data) +{ + const char *id = key; + const char *current_config_id = user_data; + NetplanConfigData *cd = value; + + if (current_config_id == NULL) + cd->invalidated = FALSE; + else if (g_strcmp0(id, current_config_id)) + cd->invalidated = TRUE; +} + +static int +terminate_try_child_process(int status, NetplanData *d, const char *config_id) +{ + sd_bus_message *msg = NULL; + g_autofree gchar *path = NULL; + int r = 0; + + if (!WIFEXITED(status)) + fprintf(stderr, "'netplan try' exited with status: %d\n", WEXITSTATUS(status)); // LCOV_EXCL_LINE + + /* Cleanup current 'netplan try' child process */ + sd_event_source_unref(d->try_es); + d->try_es = NULL; + g_spawn_close_pid (d->try_pid); + d->try_pid = -1; /* unlock semaphore */ + + /* Send .Changed() signal on DBus */ + if (config_id) { + path = g_strdup_printf("/io/netplan/Netplan/config/%s", config_id); + r = sd_bus_message_new_signal(d->bus, &msg, path, + "io.netplan.Netplan.Config", "Changed"); + } + + if (r < 0) { + // LCOV_EXCL_START + fprintf(stderr, "Could not create .Changed() signal: %s\n", strerror(-r)); + return r; + // LCOV_EXCL_STOP + } + + r = sd_bus_send(d->bus, msg, NULL); + if (r < 0) + fprintf(stderr, "Could not send .Changed() signal: %s\n", strerror(-r)); // LCOV_EXCL_LINE + sd_bus_message_unrefp(&msg); + return r; +} + +static int +_try_accept(bool accept, sd_bus_message *m, NetplanData *d, sd_bus_error *ret_error) +{ + g_autoptr(GError) error = NULL; + int status = -1; + int signal = SIGUSR1; + if (!accept) signal = SIGINT; + + /* Do not send the accept/reject signal, if this call is for another config state */ + if (d->handler_id != NULL && g_strcmp0(d->config_id, d->handler_id)) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "Another 'netplan try' process is already running"); + + /* ATTENTION: There might be a race here: + * When this accept/reject method is called at the same time as the 'netplan try' + * python process is reverting and closing itself. Not sure what to do about it... + * Maybe this needs to be fixed in python code, so that the + * 'netplan.terminal.InputRejected' exception (i.e. self-revert) cannot be + * interrupted by another exception/signal */ + + /* Send confirm (SIGUSR1) or cancel (SIGINT) signal to 'netplan try' process. + * Wait for the child process to stop, synchronously. + * Check return code/errors. */ + kill(d->try_pid, signal); + waitpid(d->try_pid, &status, 0); + g_spawn_check_exit_status(status, &error); + if (error != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan try failed: %s", error->message); // LCOV_EXCL_LINE + + terminate_try_child_process(status, d, d->config_id); + return sd_bus_reply_method_return(m, "b", true); +} + +static int +_copy_yaml_state(char *src_root, char *dst_root, sd_bus_error *ret_error) +{ + glob_t gl; + g_autoptr(GError) err = NULL; + int r = find_yaml_glob(src_root, &gl); + if (!!r) + // LCOV_EXCL_START + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "Failed glob for YAML files\n"); + // LCOV_EXCL_STOP + + /* Copy all *.yaml files from "/SRC_ROOT/{etc,run,lib}/netplan/" to + * "/DST_ROOT/{etc,run,lib}/netplan/" */ + GFile *source = NULL; + GFile *dest = NULL; + gchar *dest_path = NULL; + size_t len = strlen(src_root); + for (size_t i = 0; i < gl.gl_pathc; ++i) { + dest_path = g_strjoin(NULL, dst_root, (gl.gl_pathv[i])+len, NULL); + source = g_file_new_for_path(gl.gl_pathv[i]); + dest = g_file_new_for_path(dest_path); + g_file_copy(source, dest, G_FILE_COPY_OVERWRITE + |G_FILE_COPY_NOFOLLOW_SYMLINKS + |G_FILE_COPY_ALL_METADATA, + NULL, NULL, NULL, &err); + if (err != NULL) { + // LCOV_EXCL_START + r = sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "Failed to copy file %s -> %s: %s\n", + g_file_get_path(source), g_file_get_path(dest), + err->message); + g_object_unref(source); + g_object_unref(dest); + g_free(dest_path); + globfree(&gl); + return r; + // LCOV_EXCL_STOP + } + g_object_unref(source); + g_object_unref(dest); + g_free(dest_path); + } + globfree(&gl); + return r; +} + +static bool +_clear_tmp_state(const char *config_id, NetplanData *d) +{ + g_autofree gchar *rootdir = NULL; + /* Remove tmp YAML files */ + rootdir = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), config_id); + unlink_glob(rootdir, "/{etc,run,lib}/netplan/*.yaml"); + + /* Remove tmp state directories */ + char *subdir = NULL; + for (int i = 0; i < 3; i++) { + subdir = g_strdup_printf("%s/%s/netplan", rootdir, NETPLAN_SUBDIRS[i]); + rmdir(subdir); + g_free(subdir); + subdir = g_strdup_printf("%s/%s", rootdir, NETPLAN_SUBDIRS[i]); + rmdir(subdir); + g_free(subdir); + } + rmdir(rootdir); + + /* No cleanup of DBus object needed, if config_id points to NETPLAN_GLOBAL_CONFIG (backup) */ + if (config_id != NETPLAN_GLOBAL_CONFIG) { + /* Clear config object from DBus, by unref the appropriate slot */ + NetplanConfigData *cd = g_hash_table_lookup(d->config_data, config_id); + sd_bus_slot_unref(cd->slot); /* Clear value/slot */ + g_free(cd); /* Clear value/struct */ + g_hash_table_remove(d->config_data, config_id); /* Clear key */ + d->config_dirty = NULL; + /* TODO: HashTable error handling */ + } + + return TRUE; +} + +/** + * io.netplan.Netplan methods + */ + +static int +method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + g_autoptr(GError) err = NULL; + g_autofree gchar *stdout = NULL; + g_autofree gchar *stderr = NULL; + gint exit_status = 0; + NetplanData *d = userdata; + + /* Accept the current 'netplan try', if active. + * Otherwise execute 'netplan apply' directly. */ + if (d->try_pid > 0) + return _try_accept(TRUE, m, userdata, ret_error); + + gchar *argv[] = {SBINDIR "/" "netplan", "apply", NULL}; + + // for tests only: allow changing what netplan to run + if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); + + g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err); + // LCOV_EXCL_START + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "cannot run netplan apply: %s", err->message); + g_spawn_check_exit_status(exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "netplan apply failed: %s\nstdout: '%s'\nstderr: '%s'", + err->message, stdout, stderr); + // LCOV_EXCL_STOP + + return sd_bus_reply_method_return(m, "b", true); +} + +static int +method_generate(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + g_autoptr(GError) err = NULL; + g_autofree gchar *stdout = NULL; + g_autofree gchar *stderr = NULL; + gint exit_status = 0; + + gchar *argv[] = {SBINDIR "/" "netplan", "generate", NULL}; + + // for tests only: allow changing what netplan to run + if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); + + g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err); + // LCOV_EXCL_START + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "cannot run netplan generate: %s", err->message); + g_spawn_check_exit_status(exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "netplan generate failed: %s\nstdout: '%s'\nstderr: '%s'", + err->message, stdout, stderr); + // LCOV_EXCL_STOP + + return sd_bus_reply_method_return(m, "b", true); +} + +static int +method_info(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + sd_bus_message *reply = NULL; + gint exit_status = 0; + + exit_status = sd_bus_message_new_method_return(m, &reply); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + exit_status = sd_bus_message_open_container(reply, 'a', "(sv)"); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + exit_status = sd_bus_message_open_container(reply, 'r', "sv"); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + exit_status = sd_bus_message_append(reply, "s", "Features"); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + exit_status = sd_bus_message_open_container(reply, 'v', "as"); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + exit_status = sd_bus_message_append_strv(reply, (char**)feature_flags); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + exit_status = sd_bus_message_close_container(reply); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + exit_status = sd_bus_message_close_container(reply); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + exit_status = sd_bus_message_close_container(reply); + if (exit_status < 0) + return exit_status; // LCOV_EXCL_LINE + + return sd_bus_send(NULL, reply, NULL); +} + +static int +method_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + NetplanData *d = userdata; + g_autoptr(GError) err = NULL; + g_autofree gchar *stdout = NULL; + g_autofree gchar *stderr = NULL; + g_autofree gchar *root_dir = NULL; + gint exit_status = 0; + + if (d->config_id) + root_dir = g_strdup_printf("--root-dir=%s/netplan-config-%s", g_get_tmp_dir(), d->config_id); + gchar *argv[] = {SBINDIR "/" "netplan", "get", "all", root_dir, NULL}; + + // for tests only: allow changing what netplan to run + if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); + + g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan get: %s", err->message); // LCOV_EXCL_LINE + + g_spawn_check_exit_status(exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan get failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE + + return sd_bus_reply_method_return(m, "s", stdout); +} + +static int +method_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + NetplanData *d = userdata; + g_autoptr(GError) err = NULL; + g_autofree gchar *stdout = NULL; + g_autofree gchar *stderr = NULL; + g_autofree gchar *origin = NULL; + g_autofree gchar *root_dir = NULL; + gint exit_status = 0; + char *args[2] = {NULL, NULL}; + char *config_delta = NULL; + char *origin_hint = NULL; + guint cur_arg = 0; + + if (sd_bus_message_read(m, "ss", &config_delta, &origin_hint) < 0) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot extract config_delta or origin_hint"); // LCOV_EXCL_LINE + + if (!!strcmp(origin_hint, "")) { + origin = g_strdup_printf("--origin-hint=%s", origin_hint); + args[cur_arg] = origin; + cur_arg++; + } + + if (d->config_id) { + root_dir = g_strdup_printf("--root-dir=%s/netplan-config-%s", g_get_tmp_dir(), d->config_id); + args[cur_arg] = root_dir; + cur_arg++; + } + gchar *argv[] = {SBINDIR "/" "netplan", "set", config_delta, args[0], args[1], NULL}; + + // for tests only: allow changing what netplan to run + if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); + + g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &stdout, &stderr, &exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan set %s: %s", config_delta, err->message); // LCOV_EXCL_LINE + + g_spawn_check_exit_status(exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan set failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE + + return sd_bus_reply_method_return(m, "b", true); +} + +static int +netplan_try_cancelled_cb(sd_event_source *es, const siginfo_t *si, void* userdata) +{ + NetplanData *d = userdata; + g_autofree gchar *state_dir = NULL; + int r = 0; + if (d->handler_id) { + /* Delete GLOBAL state */ + unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml"); + /* Restore GLOBAL backup config state to main rootdir */ + state_dir = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG); + _copy_yaml_state(state_dir, NETPLAN_ROOT, NULL); + + /* Un-invalidate all other current config objects */ + if (!g_strcmp0(d->handler_id, d->config_dirty)) + g_hash_table_foreach(d->config_data, invalidate_other_config, NULL); + + /* Clear GLOBAL backup and config state */ + _clear_tmp_state(NETPLAN_GLOBAL_CONFIG, d); + _clear_tmp_state(d->handler_id, d); + } + + r = terminate_try_child_process(si->si_status, d, d->handler_id); + /* free and reset handler_id, i.e. copy of config state ID */ + g_free(d->handler_id); + d->handler_id = NULL; /* unlock pending config ID */ + return r; +} + +static int +method_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + g_autoptr(GError) err = NULL; + g_autofree gchar *timeout = NULL; + gint child_stdin = -1; /* child process needs an input to function correctly */ + guint seconds = 0; + int r = -1; + NetplanData *d = userdata; + + if (sd_bus_message_read_basic (m, 'u', &seconds) < 0) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot extract timeout_seconds"); // LCOV_EXCL_LINE + if (seconds > 0) + timeout = g_strdup_printf("--timeout=%u", seconds); + gchar *argv[] = {SBINDIR "/" "netplan", "try", timeout, NULL}; + + // for tests only: allow changing what netplan to run + if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("DBUS_TEST_NETPLAN_CMD"); + + /* Launch 'netplan try' child process, lock 'try_pid' to real PID */ + g_spawn_async_with_pipes("/", argv, NULL, + G_SPAWN_DO_NOT_REAP_CHILD|G_SPAWN_STDOUT_TO_DEV_NULL, + NULL, NULL, &d->try_pid, &child_stdin, NULL, NULL, &err); + if (err) + // LCOV_EXCL_START + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "cannot run netplan try: %s", err->message); + // LCOV_EXCL_STOP + + /* Register an event handler, trigged when the child process exits */ + if (d->config_id) + d->handler_id = g_strdup(d->config_id); /* to free in event handler */ + r = sd_event_add_child(sd_bus_get_event(d->bus), &d->try_es, d->try_pid, + WEXITED, netplan_try_cancelled_cb, d); + if (r < 0) + // LCOV_EXCL_START + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "cannot watch 'netplan try' child: %s", strerror(-r)); + // LCOV_EXCL_STOP + + return sd_bus_reply_method_return(m, "b", true); +} + +/** + * io.netplan.Netplan.Config methods + */ + +/* netplan-feature: dbus-config */ +static int +method_config_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + NetplanData *d = userdata; + g_autofree gchar *state_dir = NULL; + int r = 0; + /* trim 27 chars (i.e. "/io/netplan/Netplan/config/") from path to get the config ID */ + d->config_id = sd_bus_message_get_path(m) + 27; + NetplanConfigData *cd = g_hash_table_lookup(d->config_data, d->config_id); + if (cd->invalidated) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "This config was invalidated by another config object\n"); + /* Invalidate all other current config objects */ + g_hash_table_foreach(d->config_data, invalidate_other_config, (void*)d->config_id); + d->config_dirty = g_strdup(d->config_id); + + if (d->try_pid < 0) { + /* Delete GLOBAL state */ + unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml"); + /* Copy current config state to GLOBAL */ + state_dir = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), d->config_id); + _copy_yaml_state(state_dir, NETPLAN_ROOT, ret_error); + d->handler_id = g_strdup(d->config_id); + } + + r = method_apply(m, d, ret_error); + _clear_tmp_state(d->config_id, d); + + /* unlock current config ID and handler ID */ + d->config_id = NULL; + g_free(d->handler_id); + d->handler_id = NULL; + return r; +} + +static int +method_config_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + NetplanData *d = userdata; + /* trim 27 chars (i.e. "/io/netplan/Netplan/config/") from path to get the config ID */ + d->config_id = sd_bus_message_get_path(m) + 27; + int r = method_get(m, userdata, ret_error); + /* Reset config_id for next method call */ + d->config_id = NULL; + return r; +} + +static int +method_config_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + NetplanData *d = userdata; + /* trim 27 chars (i.e. "/io/netplan/Netplan/config/") from path to get the config ID */ + d->config_id = sd_bus_message_get_path(m) + 27; + NetplanConfigData *cd = g_hash_table_lookup(d->config_data, d->config_id); + if (cd->invalidated) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "This config was invalidated by another config object\n"); + int r = method_set(m, d, ret_error); + /* Invalidate all other current config objects */ + g_hash_table_foreach(d->config_data, invalidate_other_config, (void*)d->config_id); + d->config_dirty = g_strdup(d->config_id); + /* Reset config_id for next method call */ + d->config_id = NULL; + return r; +} + +static int +method_config_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + NetplanData *d = userdata; + g_autofree gchar *path = NULL; + g_autofree gchar *state_dir = NULL; + const char *config_id = sd_bus_message_get_path(m) + 27; + if (d->try_pid > 0) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "Another Try() is currently in progress: PID %d\n", d->try_pid); + NetplanConfigData *cd = g_hash_table_lookup(d->config_data, config_id); + if (cd->invalidated) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "This config was invalidated by another config object\n"); + + int r = 0; + /* Lock current child process temporarily until we have a real PID */ + d->try_pid = G_MAXINT; + d->config_id = config_id; + + /* Backup GLOBAL state */ + path = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG); + /* Create {etc,run,lib} subdirs with owner r/w permissions */ + char *subdir = NULL; + for (int i = 0; i < 3; i++) { + subdir = g_strdup_printf("%s/%s/netplan", path, NETPLAN_SUBDIRS[i]); + r = g_mkdir_with_parents(subdir, 0700); + if (r < 0) + // LCOV_EXCL_START + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "Failed to create '%s': %s\n", subdir, strerror(errno)); + // LCOV_EXCL_STOP + g_free(subdir); + } + + /* Copy main *.yaml files from /{etc,run,lib}/netplan/ to GLOBAL backup dir */ + _copy_yaml_state(NETPLAN_ROOT, path, ret_error); + + /* Clear main *.yaml files */ + unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml"); + + /* Copy current config *.yaml state to main rootdir (i.e. /etc/netplan/) */ + state_dir = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), d->config_id); + _copy_yaml_state(state_dir, NETPLAN_ROOT, ret_error); + + /* Exec try */ + r = method_try(m, userdata, ret_error); + d->config_id = NULL; + return r; +} + +static int +method_config_cancel(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + NetplanData *d = userdata; + g_autofree gchar *state_dir = NULL; + int r = 0; + /* trim 27 chars (i.e. "/io/netplan/Netplan/config/") from path to get the config ID */ + d->config_id = sd_bus_message_get_path(m) + 27; + if (!g_strcmp0(d->config_id, d->config_dirty)) + /* Un-invalidate all other current config objects */ + g_hash_table_foreach(d->config_data, invalidate_other_config, NULL); + + /* Cancel the current 'netplan try' process */ + if (d->try_pid > 0) + r = _try_accept(FALSE, m, d, ret_error); + else + r = sd_bus_reply_method_return(m, "b", true); + + if (d->handler_id && !g_strcmp0(d->config_id, d->handler_id)) { + /* Delete GLOBAL state */ + unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml"); + /* Restore GLOBAL backup config state to main rootdir */ + state_dir = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG); + _copy_yaml_state(state_dir, NETPLAN_ROOT, ret_error); + + /* Clear GLOBAL backup and config state */ + _clear_tmp_state(NETPLAN_GLOBAL_CONFIG, d); + + /* Clear pending Try() handler ID */ + g_free(d->handler_id); + d->handler_id = NULL; + } + + /* Clear tmp state */ + _clear_tmp_state(d->config_id, d); + d->config_id = NULL; + return r; +} + +static const sd_bus_vtable config_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Apply", "", "b", method_config_apply, 0), + SD_BUS_METHOD("Get", "", "s", method_config_get, 0), + SD_BUS_METHOD("Set", "ss", "b", method_config_set, 0), + SD_BUS_METHOD("Try", "u", "b", method_config_try, 0), + SD_BUS_METHOD("Cancel", "", "b", method_config_cancel, 0), + SD_BUS_VTABLE_END +}; + +/** + * Link between io.netplan.Netplan and io.netplan.Netplan.Config + */ + +static int +method_config(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + NetplanData *d = userdata; + sd_bus_slot *slot = NULL; + g_autoptr(GError) err = NULL; + g_autofree gchar *path = NULL; + int r = 0; + + /* Create temp. directory, according to "netplan-config-XXXXXX" template */ + path = g_dir_make_tmp("netplan-config-XXXXXX", &err); + if (err != NULL) + // LCOV_EXCL_START + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "Failed to create temp dir: %s\n", err->message); + // LCOV_EXCL_STOP + + /* Extract the last 6 randomly generated chars (i.e. "XXXXXX" from template) */ + const char *id = path + strlen(path) - 6; + const char *obj_path = g_strdup_printf("/io/netplan/Netplan/config/%s", id); + r = sd_bus_add_object_vtable(d->bus, &slot, obj_path, + "io.netplan.Netplan.Config", config_vtable, d); + // LCOV_EXCL_START + if (r < 0) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "Failed to add 'config' object: %s\n", strerror(-r)); + NetplanConfigData *cd = g_new0(NetplanConfigData, 1); + cd->slot = slot; + /* Cannot Set()/Apply() if another Set() is currently pending */ + cd->invalidated = d->config_dirty ? TRUE : FALSE; + if (!g_hash_table_insert(d->config_data, g_strdup(id), cd)) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "Failed to add object data to HashTable\n"); + // LCOV_EXCL_STOP + + /* Create {etc,run,lib} subdirs with owner r/w permissions */ + char *subdir = NULL; + for (int i = 0; i < 3; i++) { + subdir = g_strdup_printf("%s/%s/netplan", path, NETPLAN_SUBDIRS[i]); + r = g_mkdir_with_parents(subdir, 0700); + if (r < 0) + // LCOV_EXCL_START + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "Failed to create '%s': %s\n", subdir, strerror(errno)); + // LCOV_EXCL_STOP + g_free(subdir); + } + + /* Copy all *.yaml files from /{etc,run,lib}/netplan/ to temp dir */ + _copy_yaml_state(NETPLAN_ROOT, path, ret_error); + + return sd_bus_reply_method_return(m, "o", obj_path); +} + +static const sd_bus_vtable netplan_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Apply", "", "b", method_apply, 0), + SD_BUS_METHOD("Generate", "", "b", method_generate, 0), + SD_BUS_METHOD("Info", "", "a(sv)", method_info, 0), + SD_BUS_METHOD("Config", "", "o", method_config, 0), + SD_BUS_VTABLE_END +}; + +/** + * DBus setup + */ + +static int +terminate_mainloop_cb(sd_event_source *es, const struct signalfd_siginfo *si, void* userdata) { + sd_event *event = userdata; + /* Gracefully terminate the mainloop, to write GCOV output */ + sd_event_exit(event, 0); + return 0; +} + +int +main(int argc, char *argv[]) +{ + sd_bus_slot *slot = NULL; + sd_bus *bus = NULL; + sd_event *event = NULL; + NetplanData *data = g_new0(NetplanData, 1); + sigset_t mask; + int r; + + // for tests only: allow changing which rootdir to use to copy files around + if (getenv("DBUS_TEST_NETPLAN_ROOT") != 0) + NETPLAN_ROOT = getenv("DBUS_TEST_NETPLAN_ROOT"); + + /* TODO: consider sd_bus_default(&bus) for easier testing on session/user bus */ + r = sd_bus_open_system(&bus); + if (r < 0) { + // LCOV_EXCL_START + fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r)); + goto finish; + // LCOV_EXCL_STOP + } + + r = sd_event_new(&event); + if (r < 0) { + // LCOV_EXCL_START + fprintf(stderr, "Failed to create event loop: %s\n", strerror(-r)); + goto finish; + // LCOV_EXCL_STOP + } + + /* Initialize the userdata */ + data->bus = bus; + data->try_pid = -1; + data->config_id = NULL; + data->handler_id = NULL; + data->config_dirty = NULL; + /* TODO: define a proper free/cleanup function for sd_bus_slot_unref() */ + data->config_data = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + + r = sd_bus_add_object_vtable(bus, &slot, + "/io/netplan/Netplan", /* object path */ + "io.netplan.Netplan", /* interface name */ + netplan_vtable, + data); + if (r < 0) { + // LCOV_EXCL_START + fprintf(stderr, "Failed to issue method call: %s\n", strerror(-r)); + goto finish; + // LCOV_EXCL_STOP + } + + r = sd_bus_request_name(bus, "io.netplan.Netplan", 0); + if (r < 0) { + fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-r)); + goto finish; + } + + r = sd_bus_attach_event(bus, event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) { + // LCOV_EXCL_START + fprintf(stderr, "Failed to attach event loop: %s\n", strerror(-r)); + goto finish; + // LCOV_EXCL_STOP + } + + /* Mask the SIGCHLD signal, so we can listen to it via mainloop */ + sigemptyset(&mask); + sigaddset(&mask, SIGCHLD); + sigaddset(&mask, SIGTERM); + sigprocmask(SIG_BLOCK, &mask, NULL); + + /* Start the event loop, wait for requests */ + sd_event_add_signal(event, NULL, SIGTERM, terminate_mainloop_cb, event); + r = sd_event_loop(event); + if (r < 0) + fprintf(stderr, "Failed mainloop: %s\n", strerror(-r)); // LCOV_EXCL_LINE +finish: + g_free(data); + sd_event_unref(event); + sd_bus_slot_unref(slot); + sd_bus_unref(bus); + /* TODO: unref all slots from HashTable */ + + return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/src/error.c b/src/error.c new file mode 100644 index 0000000..0c34e17 --- /dev/null +++ b/src/error.c @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2019 Canonical, Ltd. + * Author: Mathieu Trudel-Lapierre + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include + +#include "parse.h" + + +/**************************************************** + * Loading and error handling + ****************************************************/ + +static void +write_error_marker(GString *message, int column) +{ + int i; + + for (i = 0; (column > 0 && i < column); i++) + g_string_append_printf(message, " "); + + g_string_append_printf(message, "^"); +} + +static char * +get_syntax_error_context(const int line_num, const int column, GError **error) +{ + GString *message = NULL; + GFile *cur_file = g_file_new_for_path(current_file); + GFileInputStream *file_stream; + GDataInputStream *stream; + gsize len; + gchar* line = NULL; + + message = g_string_sized_new(200); + file_stream = g_file_read(cur_file, NULL, error); + stream = g_data_input_stream_new (G_INPUT_STREAM(file_stream)); + g_object_unref(file_stream); + + for (int i = 0; i < line_num + 1; i++) { + g_free(line); + line = g_data_input_stream_read_line(stream, &len, NULL, error); + } + g_string_append_printf(message, "%s\n", line); + + write_error_marker(message, column); + + g_object_unref(stream); + g_object_unref(cur_file); + + return g_string_free(message, FALSE); +} + +static char * +get_parser_error_context(const yaml_parser_t *parser, GError **error) +{ + GString *message = NULL; + unsigned char* line = parser->buffer.pointer; + unsigned char* current = line; + + message = g_string_sized_new(200); + + while (current > parser->buffer.start) { + current--; + if (*current == '\n') { + line = current + 1; + break; + } + } + if (current <= parser->buffer.start) + line = parser->buffer.start; + current = line + 1; + while (current <= parser->buffer.last) { + if (*current == '\n') { + *current = '\0'; + break; + } + current++; + } + + g_string_append_printf(message, "%s\n", line); + + write_error_marker(message, parser->problem_mark.column); + + return g_string_free(message, FALSE); +} + +gboolean +parser_error(const yaml_parser_t* parser, const char* yaml, GError** error) +{ + char *error_context = get_parser_error_context(parser, error); + if ((char)*parser->buffer.pointer == '\t') + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Invalid YAML: tabs are not allowed for indent:\n%s", + yaml, + parser->problem_mark.line + 1, + parser->problem_mark.column + 1, + error_context); + else if (((char)*parser->buffer.pointer == ' ' || (char)*parser->buffer.pointer == '\0') + && !parser->token_available) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Invalid YAML: aliases are not supported:\n%s", + yaml, + parser->problem_mark.line + 1, + parser->problem_mark.column + 1, + error_context); + else if (parser->state == YAML_PARSE_BLOCK_MAPPING_KEY_STATE) + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Invalid YAML: inconsistent indentation:\n%s", + yaml, + parser->problem_mark.line + 1, + parser->problem_mark.column + 1, + error_context); + else { + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Invalid YAML: %s:\n%s", + yaml, + parser->problem_mark.line + 1, + parser->problem_mark.column + 1, + parser->problem, + error_context); + } + g_free(error_context); + + return FALSE; +} + +/** + * Put a YAML specific error message for @node into @error. + */ +gboolean +yaml_error(const yaml_node_t* node, GError** error, const char* msg, ...) +{ + va_list argp; + char* s; + char* error_context = NULL; + + va_start(argp, msg); + g_vasprintf(&s, msg, argp); + if (node != NULL) { + error_context = get_syntax_error_context(node->start_mark.line, node->start_mark.column, error); + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "%s:%zu:%zu: Error in network definition: %s\n%s", + current_file, + node->start_mark.line + 1, + node->start_mark.column + 1, + s, + error_context); + } else { + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE, + "Error in network definition: %s", s); + } + g_free(s); + va_end(argp); + return FALSE; +} + diff --git a/src/error.h b/src/error.h new file mode 100644 index 0000000..68061d8 --- /dev/null +++ b/src/error.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 Canonical, Ltd. + * Author: Mathieu Trudel-Lapierre + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include + + +gboolean +parser_error(const yaml_parser_t* parser, const char* yaml, GError** error); + +gboolean +yaml_error(const yaml_node_t* node, GError** error, const char* msg, ...); diff --git a/src/generate.c b/src/generate.c new file mode 100644 index 0000000..cccd47a --- /dev/null +++ b/src/generate.c @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2016 Canonical, Ltd. + * Author: Martin Pitt + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "util.h" +#include "parse.h" +#include "networkd.h" +#include "nm.h" +#include "openvswitch.h" +#include "sriov.h" + +static gchar* rootdir; +static gchar** files; +static gboolean any_networkd; +static gboolean any_sriov; +static gchar* mapping_iface; + +static GOptionEntry options[] = { + {"root-dir", 'r', 0, G_OPTION_ARG_FILENAME, &rootdir, "Search for and generate configuration files in this root directory instead of /"}, + {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &files, "Read configuration from this/these file(s) instead of /etc/netplan/*.yaml", "[config file ..]"}, + {"mapping", 0, 0, G_OPTION_ARG_STRING, &mapping_iface, "Only show the device to backend mapping for the specified interface."}, + {NULL} +}; + +static void +reload_udevd(void) +{ + const gchar *argv[] = { "/bin/udevadm", "control", "--reload", NULL }; + g_spawn_sync(NULL, (gchar**)argv, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, NULL, NULL); +}; + +// LCOV_EXCL_START +/* covered via 'cloud-init' integration test */ +static gboolean +check_called_just_in_time() +{ + const gchar *argv[] = { "/bin/systemctl", "is-system-running", NULL }; + gchar *output = NULL; + g_spawn_sync(NULL, (gchar**)argv, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, &output, NULL, NULL, NULL); + if (output != NULL && strstr(output, "initializing") != NULL) { + g_free(output); + const gchar *argv2[] = { "/bin/systemctl", "is-active", "network.target", NULL }; + gint exit_code = 0; + g_spawn_sync(NULL, (gchar**)argv2, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, &exit_code, NULL); + /* return TRUE, if network.target is not yet active */ + return !g_spawn_check_exit_status(exit_code, NULL); + } + g_free(output); + return FALSE; +}; + +static void +start_unit_jit(gchar *unit) +{ + const gchar *argv[] = { "/bin/systemctl", "start", "--no-block", "--no-ask-password", unit, NULL }; + g_spawn_sync(NULL, (gchar**)argv, NULL, G_SPAWN_DEFAULT, NULL, NULL, NULL, NULL, NULL, NULL); +}; +// LCOV_EXCL_STOP + +static void +nd_iterator_list(gpointer value, gpointer user_data) +{ + NetplanNetDefinition* def = (NetplanNetDefinition*) value; + if (write_networkd_conf(def, (const char*) user_data)) + any_networkd = TRUE; + + write_ovs_conf(def, (const char*) user_data); + write_nm_conf(def, (const char*) user_data); + if (def->sriov_explicit_vf_count < G_MAXUINT || def->sriov_link) + any_sriov = TRUE; +} + + +static int +find_interface(gchar* interface) +{ + GPtrArray *found; + GFileInfo *info; + GFile *driver_file; + gchar *driver_path; + gchar *driver = NULL; + gpointer key, value; + GHashTableIter iter; + int ret = EXIT_FAILURE; + + found = g_ptr_array_new (); + + /* Try to get the driver name for the interface... */ + driver_path = g_strdup_printf("/sys/class/net/%s/device/driver", interface); + driver_file = g_file_new_for_path (driver_path); + info = g_file_query_info (driver_file, + G_FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, + 0, NULL, NULL); + if (info != NULL) { + /* testing for driver matching is done via autopkgtest */ + // LCOV_EXCL_START + driver = g_path_get_basename (g_file_info_get_symlink_target (info)); + g_object_unref (info); + // LCOV_EXCL_STOP + } + g_object_unref (driver_file); + g_free (driver_path); + + g_hash_table_iter_init (&iter, netdefs); + while (g_hash_table_iter_next (&iter, &key, &value)) { + NetplanNetDefinition *nd = (NetplanNetDefinition *) value; + if (!g_strcmp0(nd->set_name, interface)) + g_ptr_array_add (found, (gpointer) nd); + else if (!g_strcmp0(nd->id, interface)) + g_ptr_array_add (found, (gpointer) nd); + else if (!g_strcmp0(nd->match.original_name, interface)) + g_ptr_array_add (found, (gpointer) nd); + } + if (found->len == 0 && driver != NULL) { + /* testing for driver matching is done via autopkgtest */ + // LCOV_EXCL_START + g_hash_table_iter_init (&iter, netdefs); + while (g_hash_table_iter_next (&iter, &key, &value)) { + NetplanNetDefinition *nd = (NetplanNetDefinition *) value; + if (!g_strcmp0(nd->match.driver, driver)) + g_ptr_array_add (found, (gpointer) nd); + } + // LCOV_EXCL_STOP + } + + if (driver) + g_free (driver); // LCOV_EXCL_LINE + + if (found->len != 1) { + goto exit_find; + } + else { + const NetplanNetDefinition *nd = (NetplanNetDefinition *)g_ptr_array_index (found, 0); + g_printf("id=%s, backend=%s, set_name=%s, match_name=%s, match_mac=%s, match_driver=%s\n", + nd->id, + netplan_backend_to_name[nd->backend], + nd->set_name, + nd->match.original_name, + nd->match.mac, + nd->match.driver); + } + + ret = EXIT_SUCCESS; + +exit_find: + g_ptr_array_free (found, TRUE); + return ret; +} + +int main(int argc, char** argv) +{ + GError* error = NULL; + GOptionContext* opt_context; + /* are we being called as systemd generator? */ + gboolean called_as_generator = (strstr(argv[0], "systemd/system-generators/") != NULL); + g_autofree char* generator_run_stamp = NULL; + glob_t gl; + + /* Parse CLI options */ + opt_context = g_option_context_new(NULL); + if (called_as_generator) + g_option_context_set_help_enabled(opt_context, FALSE); + g_option_context_set_summary(opt_context, "Generate backend network configuration from netplan YAML definition."); + g_option_context_set_description(opt_context, + "This program reads the specified netplan YAML definition file(s)\n" + "or, if none are given, /etc/netplan/*.yaml.\n" + "It then generates the corresponding systemd-networkd, NetworkManager,\n" + "and udev configuration files in /run."); + g_option_context_add_main_entries(opt_context, options, NULL); + + if (!g_option_context_parse(opt_context, &argc, &argv, &error)) { + g_fprintf(stderr, "failed to parse options: %s\n", error->message); + return 1; + } + + if (called_as_generator) { + if (files == NULL || g_strv_length(files) != 3 || files[0] == NULL) { + g_fprintf(stderr, "%s can not be called directly, use 'netplan generate'.", argv[0]); + return 1; + } + generator_run_stamp = g_build_path(G_DIR_SEPARATOR_S, files[0], "netplan.stamp", NULL); + if (g_access(generator_run_stamp, F_OK) == 0) { + g_fprintf(stderr, "netplan generate already ran, remove %s to force re-run\n", generator_run_stamp); + return 0; + } + } + + /* Read all input files */ + if (files && !called_as_generator) { + for (gchar** f = files; f && *f; ++f) + process_input_file(*f); + } else if (!process_yaml_hierarchy(rootdir)) + return 1; // LCOV_EXCL_LINE + + netdefs = netplan_finish_parse(&error); + if (error) { + g_fprintf(stderr, "%s\n", error->message); + exit(1); + } + + /* Clean up generated config from previous runs */ + cleanup_networkd_conf(rootdir); + cleanup_nm_conf(rootdir); + cleanup_ovs_conf(rootdir); + cleanup_sriov_conf(rootdir); + + if (mapping_iface && netdefs) + return find_interface(mapping_iface); + + /* Generate backend specific configuration files from merged data. */ + write_ovs_conf_finish(rootdir); // OVS cleanup unit is always written + if (netdefs) { + g_debug("Generating output files.."); + g_list_foreach (netdefs_ordered, nd_iterator_list, rootdir); + write_nm_conf_finish(rootdir); + if (any_sriov) write_sriov_conf_finish(rootdir); + /* We may have written .rules & .link files, thus we must + * invalidate udevd cache of its config as by default it only + * invalidates cache at most every 3 seconds. Not sure if this + * should live in `generate' or `apply', but it is confusing + * when udevd ignores just-in-time created rules files. + */ + reload_udevd(); + } + + /* Disable /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf + * (which restricts NM to wifi and wwan) if global renderer is NM */ + if (netplan_get_global_backend() == NETPLAN_BACKEND_NM) + g_string_free_to_file(g_string_new(NULL), rootdir, "/run/NetworkManager/conf.d/10-globally-managed-devices.conf", NULL); + + if (called_as_generator) { + /* Ensure networkd starts if we have any configuration for it */ + if (any_networkd) + enable_networkd(files[0]); + + /* Leave a stamp file so that we don't regenerate the configuration + * multiple times and userspace can wait for it to finish */ + FILE* f = fopen(generator_run_stamp, "w"); + g_assert(f != NULL); + fclose(f); + } else if (check_called_just_in_time()) { + /* netplan-feature: generate-just-in-time */ + /* When booting with cloud-init, network configuration + * might be provided just-in-time. Specifically after + * system-generators were executed, but before + * network.target is started. In such case, auxiliary + * units that netplan enables have not been included in + * the initial boot transaction. Detect such scenario and + * add all netplan units to the initial boot transaction. + */ + // LCOV_EXCL_START + /* covered via 'cloud-init' integration test */ + if (any_networkd) { + start_unit_jit("systemd-networkd.socket"); + start_unit_jit("systemd-networkd-wait-online.service"); + start_unit_jit("systemd-networkd.service"); + } + g_autofree char* glob_run = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, + "run/systemd/system/netplan-*.service", NULL); + if (!glob(glob_run, 0, NULL, &gl)) { + for (size_t i = 0; i < gl.gl_pathc; ++i) { + gchar *unit_name = g_path_get_basename(gl.gl_pathv[i]); + start_unit_jit(unit_name); + g_free(unit_name); + } + } + // LCOV_EXCL_STOP + } + + return 0; +} diff --git a/src/netplan.c b/src/netplan.c new file mode 100644 index 0000000..d941a03 --- /dev/null +++ b/src/netplan.c @@ -0,0 +1,965 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "netplan.h" +#include "parse.h" + +gchar *tmp = NULL; + +static gboolean +write_match(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + YAML_SCALAR_PLAIN(event, emitter, "match"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "name", def->match.original_name); + YAML_STRING(event, emitter, "macaddress", def->match.mac) + YAML_STRING(event, emitter, "driver", def->match.driver) + YAML_MAPPING_CLOSE(event, emitter); + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_auth(yaml_event_t* event, yaml_emitter_t* emitter, NetplanAuthenticationSettings auth) +{ + YAML_SCALAR_PLAIN(event, emitter, "auth"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "key-management", netplan_auth_key_management_type_to_str[auth.key_management]); + YAML_STRING(event, emitter, "method", netplan_auth_eap_method_to_str[auth.eap_method]); + YAML_STRING(event, emitter, "anonymous-identity", auth.anonymous_identity); + YAML_STRING(event, emitter, "identity", auth.identity); + YAML_STRING(event, emitter, "ca-certificate", auth.ca_certificate); + YAML_STRING(event, emitter, "client-certificate", auth.client_certificate); + YAML_STRING(event, emitter, "client-key", auth.client_key); + YAML_STRING(event, emitter, "client-key-password", auth.client_key_password); + YAML_STRING(event, emitter, "phase2-auth", auth.phase2_auth); + YAML_STRING(event, emitter, "password", auth.password); + YAML_MAPPING_CLOSE(event, emitter); + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_bond_params(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + if (def->bond_params.mode + || def->bond_params.monitor_interval + || def->bond_params.up_delay + || def->bond_params.down_delay + || def->bond_params.lacp_rate + || def->bond_params.transmit_hash_policy + || def->bond_params.selection_logic + || def->bond_params.arp_validate + || def->bond_params.arp_all_targets + || def->bond_params.fail_over_mac_policy + || def->bond_params.primary_reselect_policy + || def->bond_params.learn_interval + || def->bond_params.arp_interval + || def->bond_params.primary_slave + || def->bond_params.min_links + || def->bond_params.all_slaves_active + || def->bond_params.gratuitous_arp + || def->bond_params.packets_per_slave + || def->bond_params.resend_igmp + || def->bond_params.arp_ip_targets) { + YAML_SCALAR_PLAIN(event, emitter, "parameters"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "mode", def->bond_params.mode); + YAML_STRING(event, emitter, "mii-monitor-interval", def->bond_params.monitor_interval); + YAML_STRING(event, emitter, "up-delay", def->bond_params.up_delay); + YAML_STRING(event, emitter, "down-delay", def->bond_params.down_delay); + YAML_STRING(event, emitter, "lacp-rate", def->bond_params.lacp_rate); + YAML_STRING(event, emitter, "transmit-hash-policy", def->bond_params.transmit_hash_policy); + YAML_STRING(event, emitter, "ad-select", def->bond_params.selection_logic); + YAML_STRING(event, emitter, "arp-validate", def->bond_params.arp_validate); + YAML_STRING(event, emitter, "arp-all-targets", def->bond_params.arp_all_targets); + YAML_STRING(event, emitter, "fail-over-mac-policy", def->bond_params.fail_over_mac_policy); + YAML_STRING(event, emitter, "primary-reselect-policy", def->bond_params.primary_reselect_policy); + YAML_STRING(event, emitter, "learn-packet-interval", def->bond_params.learn_interval); + YAML_STRING(event, emitter, "arp-interval", def->bond_params.arp_interval); + YAML_STRING(event, emitter, "primary", def->bond_params.primary_slave); + if (def->bond_params.min_links) + YAML_UINT(event, emitter, "min-links", def->bond_params.min_links); + if (def->bond_params.all_slaves_active) + YAML_STRING_PLAIN(event, emitter, "all-slaves-active", "true"); + if (def->bond_params.gratuitous_arp) + YAML_UINT(event, emitter, "gratuitous-arp", def->bond_params.gratuitous_arp); + if (def->bond_params.packets_per_slave) + YAML_UINT(event, emitter, "packets-per-slave", def->bond_params.packets_per_slave); + if (def->bond_params.resend_igmp) + YAML_UINT(event, emitter, "resend-igmp", def->bond_params.resend_igmp); + if (def->bond_params.arp_ip_targets) { + YAML_SCALAR_PLAIN(event, emitter, "arp-ip-targets"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < def->bond_params.arp_ip_targets->len; ++i) + YAML_SCALAR_PLAIN(event, emitter, g_array_index(def->bond_params.arp_ip_targets, char*, i)); + YAML_SEQUENCE_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + } + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_bridge_params(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def, const GArray *interfaces) +{ + if (def->custom_bridging) { + gboolean has_path_cost = FALSE; + gboolean has_port_priority = FALSE; + for (unsigned i = 0; i < interfaces->len; ++i) { + NetplanNetDefinition *nd = g_array_index(interfaces, NetplanNetDefinition*, i); + has_path_cost = has_path_cost || !!nd->bridge_params.path_cost; + has_port_priority = has_port_priority || !!nd->bridge_params.port_priority; + if (has_path_cost && has_port_priority) + break; /* no need to continue this check */ + } + + YAML_SCALAR_PLAIN(event, emitter, "parameters"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "ageing-time", def->bridge_params.ageing_time); + YAML_STRING(event, emitter, "forward-delay", def->bridge_params.forward_delay); + YAML_STRING(event, emitter, "hello-time", def->bridge_params.hello_time); + YAML_STRING(event, emitter, "max-age", def->bridge_params.max_age); + if (def->bridge_params.priority) + YAML_UINT(event, emitter, "priority", def->bridge_params.priority); + if (!def->bridge_params.stp) + YAML_STRING_PLAIN(event, emitter, "stp", "false"); + + if (has_port_priority) { + YAML_SCALAR_PLAIN(event, emitter, "port-priority"); + YAML_MAPPING_OPEN(event, emitter); + for (unsigned i = 0; i < interfaces->len; ++i) { + NetplanNetDefinition *nd = g_array_index(interfaces, NetplanNetDefinition*, i); + if (nd->bridge_params.port_priority) { + YAML_UINT(event, emitter, nd->id, nd->bridge_params.port_priority); + } + } + YAML_MAPPING_CLOSE(event, emitter); + } + + if (has_path_cost) { + YAML_SCALAR_PLAIN(event, emitter, "path-cost"); + YAML_MAPPING_OPEN(event, emitter); + for (unsigned i = 0; i < interfaces->len; ++i) { + NetplanNetDefinition *nd = g_array_index(interfaces, NetplanNetDefinition*, i); + if (nd->bridge_params.path_cost) { + YAML_UINT(event, emitter, nd->id, nd->bridge_params.path_cost); + } + } + YAML_MAPPING_CLOSE(event, emitter); + } + + YAML_MAPPING_CLOSE(event, emitter); + } + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_modem_params(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + /* some modem settings to auto-detect GSM vs CDMA connections */ + if (def->modem_params.auto_config) + YAML_STRING_PLAIN(event, emitter, "auto-config", "true"); + YAML_STRING(event, emitter, "apn", def->modem_params.apn); + YAML_STRING(event, emitter, "device-id", def->modem_params.device_id); + YAML_STRING(event, emitter, "network-id", def->modem_params.network_id); + YAML_STRING(event, emitter, "pin", def->modem_params.pin); + YAML_STRING(event, emitter, "sim-id", def->modem_params.sim_id); + YAML_STRING(event, emitter, "sim-operator-id", def->modem_params.sim_operator_id); + YAML_STRING(event, emitter, "username", def->modem_params.username); + YAML_STRING(event, emitter, "password", def->modem_params.password); + YAML_STRING(event, emitter, "number", def->modem_params.number); + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +typedef struct { + yaml_event_t* event; + yaml_emitter_t* emitter; +} _passthrough_handler_data; + +static void +_passthrough_handler(GQuark key_id, gpointer value, gpointer user_data) +{ + _passthrough_handler_data *d = user_data; + const gchar* key = g_quark_to_string(key_id); + YAML_STRING(d->event, d->emitter, key, value); +error: return; // LCOV_EXCL_LINE +} + +static gboolean +write_backend_settings(yaml_event_t* event, yaml_emitter_t* emitter, NetplanBackendSettings s) { + if (s.nm.uuid || s.nm.name || s.nm.passthrough) { + YAML_SCALAR_PLAIN(event, emitter, "networkmanager"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "uuid", s.nm.uuid); + YAML_STRING(event, emitter, "name", s.nm.name); + if (s.nm.passthrough) { + YAML_SCALAR_PLAIN(event, emitter, "passthrough"); + YAML_MAPPING_OPEN(event, emitter); + _passthrough_handler_data d; + d.event = event; + d.emitter = emitter; + g_datalist_foreach(&s.nm.passthrough, _passthrough_handler, &d); + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + } + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_access_points(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + NetplanWifiAccessPoint* ap = NULL; + GHashTableIter iter; + gpointer key, value; + YAML_SCALAR_PLAIN(event, emitter, "access-points"); + YAML_MAPPING_OPEN(event, emitter); + g_hash_table_iter_init(&iter, def->access_points); + while (g_hash_table_iter_next(&iter, &key, &value)) { + ap = value; + YAML_SCALAR_QUOTED(event, emitter, ap->ssid); + YAML_MAPPING_OPEN(event, emitter); + if (ap->hidden) + YAML_STRING_PLAIN(event, emitter, "hidden", "true"); + YAML_STRING(event, emitter, "bssid", ap->bssid); + if (ap->band == NETPLAN_WIFI_BAND_5) { + YAML_STRING(event, emitter, "band", "5GHz"); + } else if (ap->band == NETPLAN_WIFI_BAND_24) { + YAML_STRING(event, emitter, "band", "2.4GHz"); + } + if (ap->channel) + YAML_UINT(event, emitter, "channel", ap->channel); + if (ap->has_auth) + write_auth(event, emitter, ap->auth); + if (ap->mode != NETPLAN_WIFI_MODE_INFRASTRUCTURE) + YAML_STRING(event, emitter, "mode", netplan_wifi_mode_to_str[ap->mode]); + if (!write_backend_settings(event, emitter, ap->backend_settings)) goto error; + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_addresses(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + YAML_SCALAR_PLAIN(event, emitter, "addresses"); + YAML_SEQUENCE_OPEN(event, emitter); + if (def->address_options) { + for (unsigned i = 0; i < def->address_options->len; ++i) { + NetplanAddressOptions *opts = g_array_index(def->address_options, NetplanAddressOptions*, i); + YAML_MAPPING_OPEN(event, emitter); + YAML_SCALAR_QUOTED(event, emitter, opts->address); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "label", opts->label); + YAML_STRING(event, emitter, "lifetime", opts->lifetime); + YAML_MAPPING_CLOSE(event, emitter); + YAML_MAPPING_CLOSE(event, emitter); + } + } + if (def->ip4_addresses) { + for (unsigned i = 0; i < def->ip4_addresses->len; ++i) + YAML_SCALAR_QUOTED(event, emitter, g_array_index(def->ip4_addresses, char*, i)); + } + if (def->ip6_addresses) { + for (unsigned i = 0; i < def->ip6_addresses->len; ++i) + YAML_SCALAR_QUOTED(event, emitter, g_array_index(def->ip6_addresses, char*, i)); + } + + YAML_SEQUENCE_CLOSE(event, emitter); + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_nameservers(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + YAML_SCALAR_PLAIN(event, emitter, "nameservers"); + YAML_MAPPING_OPEN(event, emitter); + if (def->ip4_nameservers || def->ip6_nameservers){ + YAML_SCALAR_PLAIN(event, emitter, "addresses"); + YAML_SEQUENCE_OPEN(event, emitter); + if (def->ip4_nameservers) { + for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) + YAML_SCALAR_PLAIN(event, emitter, g_array_index(def->ip4_nameservers, char*, i)); + } + if (def->ip6_nameservers) { + for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) + YAML_SCALAR_PLAIN(event, emitter, g_array_index(def->ip6_nameservers, char*, i)); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + if (def->search_domains){ + YAML_SCALAR_PLAIN(event, emitter, "search"); + YAML_SEQUENCE_OPEN(event, emitter); + if (def->search_domains) { + for (unsigned i = 0; i < def->search_domains->len; ++i) + YAML_SCALAR_PLAIN(event, emitter, g_array_index(def->search_domains, char*, i)); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_dhcp_overrides(yaml_event_t* event, yaml_emitter_t* emitter, const char* key, const NetplanDHCPOverrides data) +{ + if ( !data.use_dns + || !data.use_ntp + || !data.send_hostname + || !data.use_hostname + || !data.use_mtu + || !data.use_routes + || data.use_domains + || data.hostname + || data.metric != NETPLAN_METRIC_UNSPEC) { + YAML_SCALAR_PLAIN(event, emitter, key); + YAML_MAPPING_OPEN(event, emitter); + if (!data.use_dns) + YAML_STRING_PLAIN(event, emitter, "use-dns", "false"); + if (!data.use_ntp) + YAML_STRING_PLAIN(event, emitter, "use-ntp", "false"); + if (!data.send_hostname) + YAML_STRING_PLAIN(event, emitter, "send-hostname", "false"); + if (!data.use_hostname) + YAML_STRING_PLAIN(event, emitter, "use-hostname", "false"); + if (!data.use_mtu) + YAML_STRING_PLAIN(event, emitter, "use-mtu", "false"); + if (!data.use_routes) + YAML_STRING_PLAIN(event, emitter, "use-routes", "false"); + if (data.use_domains) + YAML_STRING(event, emitter, "use-domains", data.use_domains); + if (data.hostname) + YAML_STRING(event, emitter, "hostname", data.hostname); + if (data.metric != NETPLAN_METRIC_UNSPEC) + YAML_UINT(event, emitter, "route-metric", data.metric); + YAML_MAPPING_CLOSE(event, emitter); + } + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_tunnel_settings(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + YAML_STRING(event, emitter, "mode", netplan_tunnel_mode_to_str[def->tunnel.mode]); + YAML_STRING(event, emitter, "local", def->tunnel.local_ip); + YAML_STRING(event, emitter, "remote", def->tunnel.remote_ip); + if (def->tunnel.fwmark) + YAML_UINT(event, emitter, "mark", def->tunnel.fwmark); + if (def->tunnel.port) + YAML_UINT(event, emitter, "port", def->tunnel.port); + if (def->tunnel_ttl) + YAML_UINT(event, emitter, "ttl", def->tunnel_ttl); + + if (def->tunnel.input_key || def->tunnel.output_key || def->tunnel.private_key) { + if ( g_strcmp0(def->tunnel.input_key, def->tunnel.output_key) == 0 + && g_strcmp0(def->tunnel.input_key, def->tunnel.private_key) == 0) { + /* use short form if all keys are the same */ + YAML_STRING(event, emitter, "key", def->tunnel.input_key); + } else { + YAML_SCALAR_PLAIN(event, emitter, "keys"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "input", def->tunnel.input_key); + YAML_STRING(event, emitter, "output", def->tunnel.output_key); + YAML_STRING(event, emitter, "private", def->tunnel.private_key); + YAML_MAPPING_CLOSE(event, emitter); + } + } + + /* Wireguard peers */ + if (def->wireguard_peers && def->wireguard_peers->len > 0) { + YAML_SCALAR_PLAIN(event, emitter, "peers"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < def->wireguard_peers->len; ++i) { + NetplanWireguardPeer *peer = g_array_index(def->wireguard_peers, NetplanWireguardPeer*, i); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "endpoint", peer->endpoint); + if (peer->keepalive) + YAML_UINT(event, emitter, "keepalive", peer->keepalive); + if (peer->public_key || peer->preshared_key) { + YAML_SCALAR_PLAIN(event, emitter, "keys"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "public", peer->public_key); + YAML_STRING(event, emitter, "shared", peer->preshared_key); + YAML_MAPPING_CLOSE(event, emitter); + } + if (peer->allowed_ips && peer->allowed_ips->len > 0) { + YAML_SCALAR_PLAIN(event, emitter, "allowed-ips"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < peer->allowed_ips->len; ++i) { + char *ip = g_array_index(peer->allowed_ips, char*, i); + YAML_SCALAR_QUOTED(event, emitter, ip); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_routes(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + if (def->routes && def->routes->len > 0) { + YAML_SCALAR_PLAIN(event, emitter, "routes"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < def->routes->len; ++i) { + YAML_MAPPING_OPEN(event, emitter); + NetplanIPRoute *r = g_array_index(def->routes, NetplanIPRoute*, i); + if (r->type && g_strcmp0(r->type, "unicast") != 0) + YAML_STRING(event, emitter, "type", r->type); + if (r->scope && g_strcmp0(r->scope, "global") != 0) + YAML_STRING(event, emitter, "scope", r->scope); + if (r->metric != NETPLAN_METRIC_UNSPEC) + YAML_UINT(event, emitter, "metric", r->metric); + if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) + YAML_UINT(event, emitter, "table", r->table); + if (r->mtubytes) + YAML_UINT(event, emitter, "mtu", r->mtubytes); + if (r->congestion_window) + YAML_UINT(event, emitter, "congestion-window", r->congestion_window); + if (r->advertised_receive_window) + YAML_UINT(event, emitter, "advertised-receive-window", r->advertised_receive_window); + if (r->onlink) + YAML_STRING(event, emitter, "on-link", "true"); + if (r->from) + YAML_STRING(event, emitter, "from", r->from); + if (r->to) + YAML_STRING(event, emitter, "to", r->to); + if (r->via) + YAML_STRING(event, emitter, "via", r->via); + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + + if (def->ip_rules && def->ip_rules->len > 0) { + YAML_SCALAR_PLAIN(event, emitter, "routing-policy"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < def->ip_rules->len; ++i) { + NetplanIPRule *r = g_array_index(def->ip_rules, NetplanIPRule*, i); + YAML_MAPPING_OPEN(event, emitter); + if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) + YAML_UINT(event, emitter, "table", r->table); + if (r->priority != NETPLAN_IP_RULE_PRIO_UNSPEC) + YAML_UINT(event, emitter, "priority", r->priority); + if (r->tos != NETPLAN_IP_RULE_TOS_UNSPEC) + YAML_UINT(event, emitter, "type-of-service", r->tos); + if (r->fwmark != NETPLAN_IP_RULE_FW_MARK_UNSPEC) + YAML_UINT(event, emitter, "mark", r->fwmark); + if (r->from) + YAML_STRING(event, emitter, "from", r->from); + if (r->to) + YAML_STRING(event, emitter, "to", r->to); + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +has_openvswitch(const NetplanOVSSettings* ovs, NetplanBackend backend, GHashTable *ovs_ports) { + return (ovs_ports && g_hash_table_size(ovs_ports) > 0) + || (ovs->external_ids && g_hash_table_size(ovs->external_ids) > 0) + || (ovs->other_config && g_hash_table_size(ovs->other_config) > 0) + || ovs->lacp + || ovs->fail_mode + || ovs->mcast_snooping + || ovs->rstp + || ovs->protocols + || (ovs->ssl.ca_certificate || ovs->ssl.client_certificate || ovs->ssl.client_key) + || (ovs->controller.connection_mode || ovs->controller.addresses) + || backend == NETPLAN_BACKEND_OVS; +} + +static gboolean +write_openvswitch(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanOVSSettings* ovs, NetplanBackend backend, GHashTable *ovs_ports) +{ + GHashTableIter iter; + gpointer key, value; + + if (has_openvswitch(ovs, backend, ovs_ports)) { + YAML_SCALAR_PLAIN(event, emitter, "openvswitch"); + YAML_MAPPING_OPEN(event, emitter); + + if (ovs_ports && g_hash_table_size(ovs_ports) > 0) { + YAML_SCALAR_PLAIN(event, emitter, "ports"); + YAML_SEQUENCE_OPEN(event, emitter); + + g_hash_table_iter_init(&iter, ovs_ports); + while (g_hash_table_iter_next (&iter, &key, &value)) { + YAML_SEQUENCE_OPEN(event, emitter); + YAML_SCALAR_PLAIN(event, emitter, key); + YAML_SCALAR_PLAIN(event, emitter, value); + YAML_SEQUENCE_CLOSE(event, emitter); + g_hash_table_iter_remove(&iter); + } + + YAML_SEQUENCE_CLOSE(event, emitter); + } + + if (ovs->external_ids && g_hash_table_size(ovs->external_ids) > 0) { + YAML_SCALAR_PLAIN(event, emitter, "external-ids"); + YAML_MAPPING_OPEN(event, emitter); + g_hash_table_iter_init(&iter, ovs->external_ids); + while (g_hash_table_iter_next (&iter, &key, &value)) { + YAML_STRING(event, emitter, key, value); + } + YAML_MAPPING_CLOSE(event, emitter); + } + if (ovs->other_config && g_hash_table_size(ovs->other_config) > 0) { + YAML_SCALAR_PLAIN(event, emitter, "other-config"); + YAML_MAPPING_OPEN(event, emitter); + g_hash_table_iter_init(&iter, ovs->other_config); + while (g_hash_table_iter_next (&iter, &key, &value)) { + YAML_STRING(event, emitter, key, value); + } + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_STRING(event, emitter, "lacp", ovs->lacp); + YAML_STRING(event, emitter, "fail-mode", ovs->fail_mode); + if (ovs->mcast_snooping) + YAML_STRING_PLAIN(event, emitter, "mcast-snooping", "true"); + if (ovs->rstp) + YAML_STRING_PLAIN(event, emitter, "rstp", "true"); + if (ovs->protocols && ovs->protocols->len > 0) { + YAML_SCALAR_PLAIN(event, emitter, "protocols"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < ovs->protocols->len; ++i) { + const gchar *proto = g_array_index(ovs->protocols, gchar*, i); + YAML_SCALAR_PLAIN(event, emitter, proto); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + if (ovs->ssl.ca_certificate || ovs->ssl.client_certificate || ovs->ssl.client_key) { + YAML_SCALAR_PLAIN(event, emitter, "ssl"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "ca-cert", ovs->ssl.ca_certificate); + YAML_STRING(event, emitter, "certificate", ovs->ssl.client_certificate); + YAML_STRING(event, emitter, "private-key", ovs->ssl.client_key); + YAML_MAPPING_CLOSE(event, emitter); + } + if (ovs->controller.connection_mode || ovs->controller.addresses) { + YAML_SCALAR_PLAIN(event, emitter, "controller"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "connection-mode", ovs->controller.connection_mode); + if (ovs->controller.addresses) { + YAML_SCALAR_PLAIN(event, emitter, "addresses"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < ovs->controller.addresses->len; ++i) { + const gchar *addr = g_array_index(ovs->controller.addresses, gchar*, i); + YAML_SCALAR_QUOTED(event, emitter, addr); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + } + + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +void +_serialize_yaml(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + GArray* tmp_arr = NULL; + GHashTableIter iter; + gpointer key, value; + + YAML_SCALAR_PLAIN(event, emitter, def->id); + YAML_MAPPING_OPEN(event, emitter); + if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) { + YAML_STRING_PLAIN(event, emitter, "renderer", "sriov"); + } else if (def->backend == NETPLAN_BACKEND_NM) { + YAML_STRING_PLAIN(event, emitter, "renderer", "NetworkManager"); + } else if (def->backend == NETPLAN_BACKEND_NETWORKD) { + YAML_STRING_PLAIN(event, emitter, "renderer", "networkd"); + } + + if (def->has_match) + write_match(event, emitter, def); + + /* Do not try to handle "unknown" connection types (full fallback/passthrough) */ + if (def->type == NETPLAN_DEF_TYPE_NM) + goto only_passthrough; + + if (def->optional) + YAML_STRING_PLAIN(event, emitter, "optional", "true"); + if (def->critical) + YAML_STRING_PLAIN(event, emitter, "critical", "true"); + + if (def->ip4_addresses || def->ip6_addresses || def->address_options) + write_addresses(event, emitter, def); + if (def->ip4_nameservers || def->ip6_nameservers || def->search_domains) + write_nameservers(event, emitter, def); + + YAML_STRING_PLAIN(event, emitter, "gateway4", def->gateway4); + YAML_STRING_PLAIN(event, emitter, "gateway6", def->gateway6); + + if (g_strcmp0(def->dhcp_identifier, "duid") != 0) + YAML_STRING(event, emitter, "dhcp-identifier", def->dhcp_identifier); + if (def->dhcp4) { + YAML_STRING_PLAIN(event, emitter, "dhcp4", "true"); + write_dhcp_overrides(event, emitter, "dhcp4-overrides", def->dhcp4_overrides); + } + if (def->dhcp6) { + YAML_STRING_PLAIN(event, emitter, "dhcp6", "true"); + write_dhcp_overrides(event, emitter, "dhcp6-overrides", def->dhcp6_overrides); + } + if (def->accept_ra == NETPLAN_RA_MODE_ENABLED) { + YAML_STRING_PLAIN(event, emitter, "accept-ra", "true"); + } else if (def->accept_ra == NETPLAN_RA_MODE_DISABLED) { + YAML_STRING_PLAIN(event, emitter, "accept-ra", "false"); + } + + YAML_STRING(event, emitter, "macaddress", def->set_mac); + YAML_STRING(event, emitter, "set-name", def->set_name); + YAML_STRING(event, emitter, "ipv6-address-generation", netplan_addr_gen_mode_to_str[def->ip6_addr_gen_mode]); + YAML_STRING(event, emitter, "ipv6-address-token", def->ip6_addr_gen_token); + if (def->ip6_privacy) + YAML_STRING_PLAIN(event, emitter, "ipv6-privacy", "true"); + if (def->ipv6_mtubytes) + YAML_UINT(event, emitter, "ipv6-mtu", def->ipv6_mtubytes); + if (def->mtubytes) + YAML_UINT(event, emitter, "mtu", def->mtubytes); + if (def->emit_lldp) + YAML_STRING_PLAIN(event, emitter, "emit-lldp", "true"); + + if (def->has_auth) + write_auth(event, emitter, def->auth); + /* activation-mode */ + if (def->activation_mode) + YAML_STRING(event, emitter, "activation-mode", def->activation_mode); + + /* SR-IOV */ + if (def->sriov_link) + YAML_STRING(event, emitter, "link", def->sriov_link->id); + if (def->sriov_explicit_vf_count < G_MAXUINT) + YAML_UINT(event, emitter, "virtual-function-count", def->sriov_explicit_vf_count); + + /* Search interfaces */ + if (def->type == NETPLAN_DEF_TYPE_BRIDGE || def->type == NETPLAN_DEF_TYPE_BOND) { + tmp_arr = g_array_new(FALSE, FALSE, sizeof(NetplanNetDefinition*)); + g_hash_table_iter_init(&iter, netdefs); + while (g_hash_table_iter_next (&iter, &key, &value)) { + NetplanNetDefinition *nd = (NetplanNetDefinition *) value; + if (g_strcmp0(nd->bond, def->id) == 0 || g_strcmp0(nd->bridge, def->id) == 0) + g_array_append_val(tmp_arr, nd); + } + if (tmp_arr->len > 0) { + YAML_SCALAR_PLAIN(event, emitter, "interfaces"); + YAML_SEQUENCE_OPEN(event, emitter); + for (unsigned i = 0; i < tmp_arr->len; ++i) { + NetplanNetDefinition *nd = g_array_index(tmp_arr, NetplanNetDefinition*, i); + YAML_SCALAR_PLAIN(event, emitter, nd->id); + } + YAML_SEQUENCE_CLOSE(event, emitter); + } + write_bond_params(event, emitter, def); + write_bridge_params(event, emitter, def, tmp_arr); + g_array_free(tmp_arr, TRUE); + } + + /* Routes */ + if (def->routes || def->ip_rules) { + write_routes(event, emitter, def); + } + + /* VLAN settings */ + if (def->type == NETPLAN_DEF_TYPE_VLAN) { + if (def->vlan_id != G_MAXUINT) + YAML_UINT(event, emitter, "id", def->vlan_id); + if (def->vlan_link) + YAML_STRING_PLAIN(event, emitter, "link", def->vlan_link->id); + } + + /* Tunnel settings */ + if (def->type == NETPLAN_DEF_TYPE_TUNNEL) { + write_tunnel_settings(event, emitter, def); + } + + /* wake-on-lan */ + if (def->wake_on_lan) + YAML_STRING_PLAIN(event, emitter, "wakeonlan", "true"); + + if (def->wowlan && def->wowlan != NETPLAN_WIFI_WOWLAN_DEFAULT) { + YAML_SCALAR_PLAIN(event, emitter, "wakeonwlan"); + YAML_SEQUENCE_OPEN(event, emitter); + /* XXX: make sure to extend if NetplanWifiWowlanFlag is extended */ + if (def->wowlan & NETPLAN_WIFI_WOWLAN_ANY) + YAML_SCALAR_PLAIN(event, emitter, "any"); + if (def->wowlan & NETPLAN_WIFI_WOWLAN_DISCONNECT) + YAML_SCALAR_PLAIN(event, emitter, "disconnect"); + if (def->wowlan & NETPLAN_WIFI_WOWLAN_MAGIC) + YAML_SCALAR_PLAIN(event, emitter, "magic_pkt"); + if (def->wowlan & NETPLAN_WIFI_WOWLAN_GTK_REKEY_FAILURE) + YAML_SCALAR_PLAIN(event, emitter, "gtk_rekey_failure"); + if (def->wowlan & NETPLAN_WIFI_WOWLAN_EAP_IDENTITY_REQ) + YAML_SCALAR_PLAIN(event, emitter, "eap_identity_req"); + if (def->wowlan & NETPLAN_WIFI_WOWLAN_4WAY_HANDSHAKE) + YAML_SCALAR_PLAIN(event, emitter, "four_way_handshake"); + if (def->wowlan & NETPLAN_WIFI_WOWLAN_RFKILL_RELEASE) + YAML_SCALAR_PLAIN(event, emitter, "rfkill_release"); + if (def->wowlan & NETPLAN_WIFI_WOWLAN_TCP) + YAML_SCALAR_PLAIN(event, emitter, "tcp"); + YAML_SEQUENCE_CLOSE(event, emitter); + } + + if (def->optional_addresses) { + YAML_SCALAR_PLAIN(event, emitter, "optional-addresses"); + YAML_SEQUENCE_OPEN(event, emitter); + if (def->optional_addresses & NETPLAN_OPTIONAL_IPV4_LL) + YAML_SCALAR_PLAIN(event, emitter, "ipv4-ll") + if (def->optional_addresses & NETPLAN_OPTIONAL_IPV6_RA) + YAML_SCALAR_PLAIN(event, emitter, "ipv6-ra") + if (def->optional_addresses & NETPLAN_OPTIONAL_DHCP4) + YAML_SCALAR_PLAIN(event, emitter, "dhcp4") + if (def->optional_addresses & NETPLAN_OPTIONAL_DHCP6) + YAML_SCALAR_PLAIN(event, emitter, "dhcp6") + if (def->optional_addresses & NETPLAN_OPTIONAL_STATIC) + YAML_SCALAR_PLAIN(event, emitter, "static") + YAML_SEQUENCE_CLOSE(event, emitter); + } + + /* Generate "link-local" if it differs from the default: "[ ipv6 ]" */ + if (!(def->linklocal.ipv6 && !def->linklocal.ipv4)) { + YAML_SCALAR_PLAIN(event, emitter, "link-local"); + YAML_SEQUENCE_OPEN(event, emitter); + if (def->linklocal.ipv4) + YAML_SCALAR_PLAIN(event, emitter, "ipv4"); + if (def->linklocal.ipv6) + YAML_SCALAR_PLAIN(event, emitter, "ipv6"); + YAML_SEQUENCE_CLOSE(event, emitter); + } + + write_openvswitch(event, emitter, &def->ovs_settings, def->backend, NULL); + + if (def->type == NETPLAN_DEF_TYPE_MODEM) + write_modem_params(event, emitter, def); + + if (def->type == NETPLAN_DEF_TYPE_WIFI) + if (!write_access_points(event, emitter, def)) goto error; + + /* Handle devices in full fallback/passthrough mode (i.e. 'nm-devices') */ +only_passthrough: + if (!write_backend_settings(event, emitter, def->backend_settings)) goto error; + + /* Close remaining mappings */ + YAML_MAPPING_CLOSE(event, emitter); + return; + + // LCOV_EXCL_START +error: + g_warning("Error generating YAML: %s", emitter->problem); + return; + // LCOV_EXCL_STOP +} + +/** + * Generate the Netplan YAML configuration for the selected netdef + * @def: NetplanNetDefinition (as pointer), the data to be serialized + * @rootdir: If not %NULL, generate configuration in this root directory + * (useful for testing). + */ +void +write_netplan_conf(const NetplanNetDefinition* def, const char* rootdir) +{ + g_autofree gchar *filename = NULL; + g_autofree gchar *path = NULL; + + /* NetworkManager produces one file per connection profile + * It's 90-* to be higher priority than the default 70-netplan-set.yaml */ + if (def->backend_settings.nm.uuid) + filename = g_strconcat("90-NM-", def->backend_settings.nm.uuid, ".yaml", NULL); + else + filename = g_strconcat("10-netplan-", def->id, ".yaml", NULL); + path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, "etc", "netplan", filename, NULL); + + /* Start rendering YAML output */ + yaml_emitter_t emitter_data; + yaml_event_t event_data; + yaml_emitter_t* emitter = &emitter_data; + yaml_event_t* event = &event_data; + FILE *output = fopen(path, "wb"); + + YAML_OUT_START(event, emitter, output); + /* build the netplan boilerplate YAML structure */ + YAML_SCALAR_PLAIN(event, emitter, "network"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING_PLAIN(event, emitter, "version", "2"); + + if (netplan_def_type_to_str[def->type]) { + YAML_SCALAR_PLAIN(event, emitter, netplan_def_type_to_str[def->type]); + YAML_MAPPING_OPEN(event, emitter); + _serialize_yaml(event, emitter, def); + YAML_MAPPING_CLOSE(event, emitter); + } + + /* Close remaining mappings */ + YAML_MAPPING_CLOSE(event, emitter); + + /* Tear down the YAML emitter */ + YAML_OUT_STOP(event, emitter); + fclose(output); + return; + + // LCOV_EXCL_START +error: + g_warning("Error generating YAML: %s", emitter->problem); + yaml_emitter_delete(emitter); + fclose(output); + // LCOV_EXCL_STOP +} + +gboolean +contains_netdef_type(gpointer key, gpointer value, gpointer user_data) +{ + NetplanNetDefinition *nd = value; + NetplanDefType *type = user_data; + return nd->type == *type; +} + +/** + * Generate the Netplan YAML configuration for all currently parsed netdefs + * @file_hint: Name hint for the generated output YAML file + * @rootdir: If not %NULL, generate configuration in this root directory + * (useful for testing). + */ +void +write_netplan_conf_full(const char* file_hint, const char* rootdir) +{ + g_autofree gchar *path = NULL; + GHashTable *ovs_ports = NULL; + GHashTableIter iter; + gpointer key, value; + + gboolean global_values = ( (netplan_get_global_backend() != NETPLAN_BACKEND_NONE) + || has_openvswitch(&ovs_settings_global, NETPLAN_BACKEND_NONE, NULL)); + + if (global_values || (netdefs && g_hash_table_size(netdefs) > 0)) { + path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, "etc", "netplan", file_hint, NULL); + + /* Start rendering YAML output */ + yaml_emitter_t emitter_data; + yaml_event_t event_data; + yaml_emitter_t* emitter = &emitter_data; + yaml_event_t* event = &event_data; + FILE *output = fopen(path, "wb"); + + YAML_OUT_START(event, emitter, output); + /* build the netplan boilerplate YAML structure */ + YAML_SCALAR_PLAIN(event, emitter, "network"); + YAML_MAPPING_OPEN(event, emitter); + /* We support version 2 only, currently */ + YAML_STRING_PLAIN(event, emitter, "version", "2"); + + if (netplan_get_global_backend() == NETPLAN_BACKEND_NM) { + YAML_STRING_PLAIN(event, emitter, "renderer", "NetworkManager"); + } else if (netplan_get_global_backend() == NETPLAN_BACKEND_NETWORKD) { + YAML_STRING_PLAIN(event, emitter, "renderer", "networkd"); + } + + /* Go through the netdefs type-by-type */ + if (netdefs && g_hash_table_size(netdefs) > 0) { + for (unsigned i = 0; i < NETPLAN_DEF_TYPE_MAX_; ++i) { + /* Per-netdef config */ + if (g_hash_table_find(netdefs, contains_netdef_type, &i)) { + if (netplan_def_type_to_str[i]) { + YAML_SCALAR_PLAIN(event, emitter, netplan_def_type_to_str[i]); + YAML_MAPPING_OPEN(event, emitter); + g_hash_table_iter_init(&iter, netdefs); + while (g_hash_table_iter_next (&iter, &key, &value)) { + NetplanNetDefinition *def = (NetplanNetDefinition *) value; + if (def->type == i) + _serialize_yaml(event, emitter, def); + } + YAML_MAPPING_CLOSE(event, emitter); + } else if (i == NETPLAN_DEF_TYPE_PORT) { + g_hash_table_iter_init(&iter, netdefs); + while (g_hash_table_iter_next (&iter, &key, &value)) { + NetplanNetDefinition *def = (NetplanNetDefinition *) value; + if (def->type == i) { + if (!ovs_ports) + ovs_ports = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + /* Insert each port:peer combination only once */ + if (!g_hash_table_lookup(ovs_ports, def->id)) + g_hash_table_insert(ovs_ports, g_strdup(def->peer), g_strdup(def->id)); + } + } + } + } + } + } + + write_openvswitch(event, emitter, &ovs_settings_global, NETPLAN_BACKEND_NONE, ovs_ports); + + /* Close remaining mappings */ + YAML_MAPPING_CLOSE(event, emitter); + + /* Tear down the YAML emitter */ + YAML_OUT_STOP(event, emitter); + fclose(output); + return; + + // LCOV_EXCL_START +error: + g_warning("Error generating YAML: %s", emitter->problem); + yaml_emitter_delete(emitter); + fclose(output); + // LCOV_EXCL_STOP + } else { + g_debug("No data/netdefs to serialize into YAML."); + } +} + +/* XXX: implement the following functions, once needed: +void write_netplan_conf_finish(const char* rootdir) +void cleanup_netplan_conf(const char* rootdir) +*/ + +/** + * Helper function for testing only + */ +void +_write_netplan_conf(const char* netdef_id, const char* rootdir) +{ + GHashTable* ht = NULL; + const NetplanNetDefinition* def = NULL; + ht = netplan_finish_parse(NULL); + def = g_hash_table_lookup(ht, netdef_id); + write_netplan_conf(def, rootdir); +} diff --git a/src/netplan.h b/src/netplan.h new file mode 100644 index 0000000..7c5706a --- /dev/null +++ b/src/netplan.h @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "parse.h" + +#define YAML_MAPPING_OPEN(event_ptr, emitter_ptr) \ +{ \ + yaml_mapping_start_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_MAP_TAG, 1, YAML_BLOCK_MAPPING_STYLE); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +#define YAML_MAPPING_CLOSE(event_ptr, emitter_ptr) \ +{ \ + yaml_mapping_end_event_initialize(event_ptr); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +#define YAML_SEQUENCE_OPEN(event_ptr, emitter_ptr) \ +{ \ + yaml_sequence_start_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_SEQ_TAG, 1, YAML_BLOCK_SEQUENCE_STYLE); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +#define YAML_SEQUENCE_CLOSE(event_ptr, emitter_ptr) \ +{ \ + yaml_sequence_end_event_initialize(event_ptr); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +#define YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, scalar) \ +{ \ + yaml_scalar_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_STR_TAG, (yaml_char_t *)scalar, strlen(scalar), 1, 0, YAML_PLAIN_SCALAR_STYLE); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +/* Implicit plain and quoted tags, double quoted style */ +#define YAML_SCALAR_QUOTED(event_ptr, emitter_ptr, scalar) \ +{ \ + yaml_scalar_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_STR_TAG, (yaml_char_t *)scalar, strlen(scalar), 1, 1, YAML_DOUBLE_QUOTED_SCALAR_STYLE); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +#define YAML_STRING(event_ptr, emitter_ptr, key, value_ptr) \ +{ \ + if (value_ptr) { \ + YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ + YAML_SCALAR_QUOTED(event_ptr, emitter_ptr, value_ptr); \ + } \ +} +#define YAML_STRING_PLAIN(event_ptr, emitter_ptr, key, value_ptr) \ +{ \ + if (value_ptr) { \ + YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, key); \ + YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, value_ptr); \ + } \ +} +#define YAML_UINT(event_ptr, emitter_ptr, key, value) \ +{ \ + tmp = g_strdup_printf("%u", value); \ + YAML_STRING_PLAIN(event_ptr, emitter_ptr, key, tmp); \ + g_free(tmp); \ +} + +/* open YAML emitter, document, stream and initial mapping */ +#define YAML_OUT_START(event_ptr, emitter_ptr, file) \ +{ \ + yaml_emitter_initialize(emitter_ptr); \ + yaml_emitter_set_output_file(emitter_ptr, file); \ + yaml_stream_start_event_initialize(event_ptr, YAML_UTF8_ENCODING); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ + yaml_document_start_event_initialize(event_ptr, NULL, NULL, NULL, 1); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ + YAML_MAPPING_OPEN(event_ptr, emitter_ptr); \ +} +/* close initial YAML mapping, document, stream and emitter */ +#define YAML_OUT_STOP(event_ptr, emitter_ptr) \ +{ \ + YAML_MAPPING_CLOSE(event_ptr, emitter_ptr); \ + yaml_document_end_event_initialize(event_ptr, 1); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ + yaml_stream_end_event_initialize(event_ptr); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ + yaml_emitter_delete(emitter_ptr); \ +} + +static const char* const netplan_def_type_to_str[NETPLAN_DEF_TYPE_MAX_] = { + [NETPLAN_DEF_TYPE_NONE] = NULL, + [NETPLAN_DEF_TYPE_ETHERNET] = "ethernets", + [NETPLAN_DEF_TYPE_WIFI] = "wifis", + [NETPLAN_DEF_TYPE_MODEM] = "modems", + [NETPLAN_DEF_TYPE_BRIDGE] = "bridges", + [NETPLAN_DEF_TYPE_BOND] = "bonds", + [NETPLAN_DEF_TYPE_VLAN] = "vlans", + [NETPLAN_DEF_TYPE_TUNNEL] = "tunnels", + [NETPLAN_DEF_TYPE_PORT] = NULL, + [NETPLAN_DEF_TYPE_NM] = "nm-devices", +}; + +static const char* const netplan_auth_key_management_type_to_str[NETPLAN_AUTH_KEY_MANAGEMENT_MAX] = { + [NETPLAN_AUTH_KEY_MANAGEMENT_NONE] = "none", + [NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK] = "psk", + [NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP] = "eap", + [NETPLAN_AUTH_KEY_MANAGEMENT_8021X] = "802.1x", +}; + +static const char* const netplan_auth_eap_method_to_str[NETPLAN_AUTH_EAP_METHOD_MAX] = { + [NETPLAN_AUTH_EAP_NONE] = NULL, + [NETPLAN_AUTH_EAP_TLS] = "tls", + [NETPLAN_AUTH_EAP_PEAP] = "peap", + [NETPLAN_AUTH_EAP_TTLS] = "ttls", +}; + +static const char* const netplan_tunnel_mode_to_str[NETPLAN_TUNNEL_MODE_MAX_] = { + [NETPLAN_TUNNEL_MODE_UNKNOWN] = NULL, + [NETPLAN_TUNNEL_MODE_IPIP] = "ipip", + [NETPLAN_TUNNEL_MODE_GRE] = "gre", + [NETPLAN_TUNNEL_MODE_SIT] = "sit", + [NETPLAN_TUNNEL_MODE_ISATAP] = "isatap", + [NETPLAN_TUNNEL_MODE_VTI] = "vti", + [NETPLAN_TUNNEL_MODE_IP6IP6] = "ip6ip6", + [NETPLAN_TUNNEL_MODE_IPIP6] = "ipip6", + [NETPLAN_TUNNEL_MODE_IP6GRE] = "ip6gre", + [NETPLAN_TUNNEL_MODE_VTI6] = "vti6", + [NETPLAN_TUNNEL_MODE_GRETAP] = "gretap", + [NETPLAN_TUNNEL_MODE_IP6GRETAP] = "ip6gretap", + [NETPLAN_TUNNEL_MODE_WIREGUARD] = "wireguard", +}; + +static const char* const netplan_addr_gen_mode_to_str[NETPLAN_ADDRGEN_MAX] = { + [NETPLAN_ADDRGEN_DEFAULT] = NULL, + [NETPLAN_ADDRGEN_EUI64] = "eui64", + [NETPLAN_ADDRGEN_STABLEPRIVACY] = "stable-privacy" +}; + +void write_netplan_conf(const NetplanNetDefinition* def, const char* rootdir); diff --git a/src/netplan.script b/src/netplan.script new file mode 100755 index 0000000..3c131f6 --- /dev/null +++ b/src/netplan.script @@ -0,0 +1,23 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +'''netplan command line''' + +from netplan import Netplan + +netplan = Netplan() +netplan.main() diff --git a/src/networkd.c b/src/networkd.c new file mode 100644 index 0000000..8884286 --- /dev/null +++ b/src/networkd.c @@ -0,0 +1,1131 @@ +/* + * Copyright (C) 2016 Canonical, Ltd. + * Author: Martin Pitt + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "networkd.h" +#include "parse.h" +#include "util.h" +#include "validation.h" + +/** + * Append WiFi frequencies to wpa_supplicant's freq_list= + */ +static void +wifi_append_freq(gpointer key, gpointer value, gpointer user_data) +{ + GString* s = user_data; + g_string_append_printf(s, "%d ", GPOINTER_TO_INT(value)); +} + +/** + * append wowlan_triggers= string for wpa_supplicant.conf + */ +static void +append_wifi_wowlan_flags(NetplanWifiWowlanFlag flag, GString* str) { + if (flag & NETPLAN_WIFI_WOWLAN_TYPES[0].flag || flag >= NETPLAN_WIFI_WOWLAN_TCP) { + g_fprintf(stderr, "ERROR: unsupported wowlan_triggers mask: 0x%x\n", flag); + exit(1); + } + for (unsigned i = 0; NETPLAN_WIFI_WOWLAN_TYPES[i].name != NULL; ++i) { + if (flag & NETPLAN_WIFI_WOWLAN_TYPES[i].flag) { + g_string_append_printf(str, "%s ", NETPLAN_WIFI_WOWLAN_TYPES[i].name); + } + } + /* replace trailing space with newline */ + str = g_string_overwrite(str, str->len-1, "\n"); +} + +/** + * Append [Match] section of @def to @s. + */ +static void +append_match_section(const NetplanNetDefinition* def, GString* s, gboolean match_rename) +{ + /* Note: an empty [Match] section is interpreted as matching all devices, + * which is what we want for the simple case that you only have one device + * (of the given type) */ + + g_string_append(s, "[Match]\n"); + if (def->match.driver) + g_string_append_printf(s, "Driver=%s\n", def->match.driver); + if (def->match.mac) + g_string_append_printf(s, "MACAddress=%s\n", def->match.mac); + /* name matching is special: if the .link renames the interface, the + * .network has to use the renamed one, otherwise the original one */ + if (!match_rename && def->match.original_name) + g_string_append_printf(s, "OriginalName=%s\n", def->match.original_name); + if (match_rename) { + if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL) + g_string_append_printf(s, "Name=%s\n", def->id); + else if (def->set_name) + g_string_append_printf(s, "Name=%s\n", def->set_name); + else if (def->match.original_name) + g_string_append_printf(s, "Name=%s\n", def->match.original_name); + } + + /* Workaround for bugs LP: #1804861 and LP: #1888726: something outputs + * netplan config that includes using the MAC of the first phy member of a + * bond as default value for the MAC of the bond device itself. This is + * evil, it's an optional field and networkd knows what to do if the MAC + * isn't specified; but work around this by adding an arbitrary additional + * match condition on Path= for the phys. This way, hopefully setting a MTU + * on the phy does not bleed over to bond/bridge and any further virtual + * devices (VLANs?) on top of it. + * Make sure to add the extra match only if we're matching by MAC + * already and dealing with a bond, bridge or vlan. + */ + if (def->bond || def->bridge || def->has_vlans) { + /* update if we support new device types */ + if (def->match.mac) + g_string_append(s, "Type=!vlan bond bridge\n"); + } +} + +static void +write_bridge_params(GString* s, const NetplanNetDefinition* def) +{ + GString *params = NULL; + + if (def->custom_bridging) { + params = g_string_sized_new(200); + + if (def->bridge_params.ageing_time) + g_string_append_printf(params, "AgeingTimeSec=%s\n", def->bridge_params.ageing_time); + if (def->bridge_params.priority) + g_string_append_printf(params, "Priority=%u\n", def->bridge_params.priority); + if (def->bridge_params.forward_delay) + g_string_append_printf(params, "ForwardDelaySec=%s\n", def->bridge_params.forward_delay); + if (def->bridge_params.hello_time) + g_string_append_printf(params, "HelloTimeSec=%s\n", def->bridge_params.hello_time); + if (def->bridge_params.max_age) + g_string_append_printf(params, "MaxAgeSec=%s\n", def->bridge_params.max_age); + g_string_append_printf(params, "STP=%s\n", def->bridge_params.stp ? "true" : "false"); + + g_string_append_printf(s, "\n[Bridge]\n%s", params->str); + + g_string_free(params, TRUE); + } +} + +static void +write_tunnel_params(GString* s, const NetplanNetDefinition* def) +{ + GString *params = NULL; + + params = g_string_sized_new(200); + + g_string_printf(params, "Independent=true\n"); + if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_IPIP6 || def->tunnel.mode == NETPLAN_TUNNEL_MODE_IP6IP6) + g_string_append_printf(params, "Mode=%s\n", tunnel_mode_to_string(def->tunnel.mode)); + g_string_append_printf(params, "Local=%s\n", def->tunnel.local_ip); + g_string_append_printf(params, "Remote=%s\n", def->tunnel.remote_ip); + if (def->tunnel_ttl) + g_string_append_printf(params, "TTL=%u\n", def->tunnel_ttl); + if (def->tunnel.input_key) + g_string_append_printf(params, "InputKey=%s\n", def->tunnel.input_key); + if (def->tunnel.output_key) + g_string_append_printf(params, "OutputKey=%s\n", def->tunnel.output_key); + + g_string_append_printf(s, "\n[Tunnel]\n%s", params->str); + g_string_free(params, TRUE); +} + +static void +write_wireguard_params(GString* s, const NetplanNetDefinition* def) +{ + GString *params = NULL; + params = g_string_sized_new(200); + + g_assert(def->tunnel.private_key); + /* The "PrivateKeyFile=" setting is available as of systemd-netwokrd v242+ + * Base64 encoded PrivateKey= or absolute PrivateKeyFile= fields are mandatory. + * + * The key was already validated via validate_tunnel_grammar(), but we need + * to differentiate between base64 key VS absolute path key-file. And a base64 + * string could (theoretically) start with '/', so we use is_wireguard_key() + * as well to check for more specific characteristics (if needed). */ + if (def->tunnel.private_key[0] == '/' && !is_wireguard_key(def->tunnel.private_key)) + g_string_append_printf(params, "PrivateKeyFile=%s\n", def->tunnel.private_key); + else + g_string_append_printf(params, "PrivateKey=%s\n", def->tunnel.private_key); + + if (def->tunnel.port) + g_string_append_printf(params, "ListenPort=%u\n", def->tunnel.port); + /* This is called FirewallMark= as of systemd v243, but we keep calling it FwMark= for + backwards compatibility. FwMark= is still supported, but deprecated: + https://github.com/systemd/systemd/pull/12478 */ + if (def->tunnel.fwmark) + g_string_append_printf(params, "FwMark=%u\n", def->tunnel.fwmark); + + g_string_append_printf(s, "\n[WireGuard]\n%s", params->str); + g_string_free(params, TRUE); + + for (guint i = 0; i < def->wireguard_peers->len; i++) { + NetplanWireguardPeer *peer = g_array_index (def->wireguard_peers, NetplanWireguardPeer*, i); + GString *peer_s = g_string_sized_new(200); + + g_string_append_printf(peer_s, "PublicKey=%s\n", peer->public_key); + g_string_append(peer_s, "AllowedIPs="); + for (guint i = 0; i < peer->allowed_ips->len; ++i) { + if (i > 0 ) + g_string_append_c(peer_s, ','); + g_string_append_printf(peer_s, "%s", g_array_index(peer->allowed_ips, char*, i)); + } + g_string_append_c(peer_s, '\n'); + + if (peer->keepalive) + g_string_append_printf(peer_s, "PersistentKeepalive=%d\n", peer->keepalive); + if (peer->endpoint) + g_string_append_printf(peer_s, "Endpoint=%s\n", peer->endpoint); + /* The key was already validated via validate_tunnel_grammar(), but we need + * to differentiate between base64 key VS absolute path key-file. And a base64 + * string could (theoretically) start with '/', so we use is_wireguard_key() + * as well to check for more specific characteristics (if needed). */ + if (peer->preshared_key) { + if (peer->preshared_key[0] == '/' && !is_wireguard_key(peer->preshared_key)) + g_string_append_printf(peer_s, "PresharedKeyFile=%s\n", peer->preshared_key); + else + g_string_append_printf(peer_s, "PresharedKey=%s\n", peer->preshared_key); + } + + g_string_append_printf(s, "\n[WireGuardPeer]\n%s", peer_s->str); + g_string_free(peer_s, TRUE); + } +} + +static void +write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char* path) +{ + GString* s = NULL; + mode_t orig_umask; + + /* Don't write .link files for virtual devices; they use .netdev instead. + * Don't write .link files for MODEM devices, as they aren't supported by networkd. + */ + if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL || def->type == NETPLAN_DEF_TYPE_MODEM) + return; + + /* do we need to write a .link file? */ + if (!def->set_name && !def->wake_on_lan && !def->mtubytes) + return; + + /* build file contents */ + s = g_string_sized_new(200); + append_match_section(def, s, FALSE); + + g_string_append(s, "\n[Link]\n"); + if (def->set_name) + g_string_append_printf(s, "Name=%s\n", def->set_name); + /* FIXME: Should this be turned from bool to str and support multiple values? */ + g_string_append_printf(s, "WakeOnLan=%s\n", def->wake_on_lan ? "magic" : "off"); + if (def->mtubytes) + g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); + + orig_umask = umask(022); + g_string_free_to_file(s, rootdir, path, ".link"); + umask(orig_umask); +} + + +static gboolean +interval_has_suffix(const char* param) { + gchar* endptr; + + g_ascii_strtoull(param, &endptr, 10); + if (*endptr == '\0') + return FALSE; + + return TRUE; +} + + +static void +write_bond_parameters(const NetplanNetDefinition* def, GString* s) +{ + GString* params = NULL; + + params = g_string_sized_new(200); + + if (def->bond_params.mode) + g_string_append_printf(params, "\nMode=%s", def->bond_params.mode); + if (def->bond_params.lacp_rate) + g_string_append_printf(params, "\nLACPTransmitRate=%s", def->bond_params.lacp_rate); + if (def->bond_params.monitor_interval) { + g_string_append(params, "\nMIIMonitorSec="); + if (interval_has_suffix(def->bond_params.monitor_interval)) + g_string_append(params, def->bond_params.monitor_interval); + else + g_string_append_printf(params, "%sms", def->bond_params.monitor_interval); + } + if (def->bond_params.min_links) + g_string_append_printf(params, "\nMinLinks=%d", def->bond_params.min_links); + if (def->bond_params.transmit_hash_policy) + g_string_append_printf(params, "\nTransmitHashPolicy=%s", def->bond_params.transmit_hash_policy); + if (def->bond_params.selection_logic) + g_string_append_printf(params, "\nAdSelect=%s", def->bond_params.selection_logic); + if (def->bond_params.all_slaves_active) + g_string_append_printf(params, "\nAllSlavesActive=%d", def->bond_params.all_slaves_active); + if (def->bond_params.arp_interval) { + g_string_append(params, "\nARPIntervalSec="); + if (interval_has_suffix(def->bond_params.arp_interval)) + g_string_append(params, def->bond_params.arp_interval); + else + g_string_append_printf(params, "%sms", def->bond_params.arp_interval); + } + if (def->bond_params.arp_ip_targets && def->bond_params.arp_ip_targets->len > 0) { + g_string_append_printf(params, "\nARPIPTargets="); + for (unsigned i = 0; i < def->bond_params.arp_ip_targets->len; ++i) { + if (i > 0) + g_string_append_printf(params, " "); + g_string_append_printf(params, "%s", g_array_index(def->bond_params.arp_ip_targets, char*, i)); + } + } + if (def->bond_params.arp_validate) + g_string_append_printf(params, "\nARPValidate=%s", def->bond_params.arp_validate); + if (def->bond_params.arp_all_targets) + g_string_append_printf(params, "\nARPAllTargets=%s", def->bond_params.arp_all_targets); + if (def->bond_params.up_delay) { + g_string_append(params, "\nUpDelaySec="); + if (interval_has_suffix(def->bond_params.up_delay)) + g_string_append(params, def->bond_params.up_delay); + else + g_string_append_printf(params, "%sms", def->bond_params.up_delay); + } + if (def->bond_params.down_delay) { + g_string_append(params, "\nDownDelaySec="); + if (interval_has_suffix(def->bond_params.down_delay)) + g_string_append(params, def->bond_params.down_delay); + else + g_string_append_printf(params, "%sms", def->bond_params.down_delay); + } + if (def->bond_params.fail_over_mac_policy) + g_string_append_printf(params, "\nFailOverMACPolicy=%s", def->bond_params.fail_over_mac_policy); + if (def->bond_params.gratuitous_arp) + g_string_append_printf(params, "\nGratuitousARP=%d", def->bond_params.gratuitous_arp); + /* TODO: add unsolicited_na, not documented as supported by NM. */ + if (def->bond_params.packets_per_slave) + g_string_append_printf(params, "\nPacketsPerSlave=%d", def->bond_params.packets_per_slave); + if (def->bond_params.primary_reselect_policy) + g_string_append_printf(params, "\nPrimaryReselectPolicy=%s", def->bond_params.primary_reselect_policy); + if (def->bond_params.resend_igmp) + g_string_append_printf(params, "\nResendIGMP=%d", def->bond_params.resend_igmp); + if (def->bond_params.learn_interval) + g_string_append_printf(params, "\nLearnPacketIntervalSec=%s", def->bond_params.learn_interval); + + if (params->len) + g_string_append_printf(s, "\n[Bond]%s\n", params->str); + + g_string_free(params, TRUE); +} + +static void +write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const char* path) +{ + GString* s = NULL; + mode_t orig_umask; + + g_assert(def->type >= NETPLAN_DEF_TYPE_VIRTUAL); + + if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) { + g_debug("%s is defined as a hardware SR-IOV filtered VLAN, postponing creation", def->id); + return; + } + + /* build file contents */ + s = g_string_sized_new(200); + g_string_append_printf(s, "[NetDev]\nName=%s\n", def->id); + + if (def->set_mac) + g_string_append_printf(s, "MACAddress=%s\n", def->set_mac); + if (def->mtubytes) + g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); + + switch (def->type) { + case NETPLAN_DEF_TYPE_BRIDGE: + g_string_append(s, "Kind=bridge\n"); + write_bridge_params(s, def); + break; + + case NETPLAN_DEF_TYPE_BOND: + g_string_append(s, "Kind=bond\n"); + write_bond_parameters(def, s); + break; + + case NETPLAN_DEF_TYPE_VLAN: + g_string_append_printf(s, "Kind=vlan\n\n[VLAN]\nId=%u\n", def->vlan_id); + break; + + case NETPLAN_DEF_TYPE_TUNNEL: + switch(def->tunnel.mode) { + case NETPLAN_TUNNEL_MODE_GRE: + case NETPLAN_TUNNEL_MODE_GRETAP: + case NETPLAN_TUNNEL_MODE_IPIP: + case NETPLAN_TUNNEL_MODE_IP6GRE: + case NETPLAN_TUNNEL_MODE_IP6GRETAP: + case NETPLAN_TUNNEL_MODE_SIT: + case NETPLAN_TUNNEL_MODE_VTI: + case NETPLAN_TUNNEL_MODE_VTI6: + case NETPLAN_TUNNEL_MODE_WIREGUARD: + g_string_append_printf(s, "Kind=%s\n", + tunnel_mode_to_string(def->tunnel.mode)); + break; + + case NETPLAN_TUNNEL_MODE_IP6IP6: + case NETPLAN_TUNNEL_MODE_IPIP6: + g_string_append(s, "Kind=ip6tnl\n"); + break; + + // LCOV_EXCL_START + default: + g_assert_not_reached(); + // LCOV_EXCL_STOP + } + if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) + write_wireguard_params(s, def); + else + write_tunnel_params(s, def); + break; + + default: g_assert_not_reached(); // LCOV_EXCL_LINE + } + + /* these do not contain secrets and need to be readable by + * systemd-networkd - LP: #1736965 */ + orig_umask = umask(022); + g_string_free_to_file(s, rootdir, path, ".netdev"); + umask(orig_umask); +} + +static void +write_route(NetplanIPRoute* r, GString* s) +{ + const char *to; + g_string_append_printf(s, "\n[Route]\n"); + + if (g_strcmp0(r->to, "default") == 0) + to = get_global_network(r->family); + else + to = r->to; + g_string_append_printf(s, "Destination=%s\n", to); + + if (r->via) + g_string_append_printf(s, "Gateway=%s\n", r->via); + if (r->from) + g_string_append_printf(s, "PreferredSource=%s\n", r->from); + + if (g_strcmp0(r->scope, "global") != 0) + g_string_append_printf(s, "Scope=%s\n", r->scope); + if (g_strcmp0(r->type, "unicast") != 0) + g_string_append_printf(s, "Type=%s\n", r->type); + if (r->onlink) + g_string_append_printf(s, "GatewayOnlink=true\n"); + if (r->metric != NETPLAN_METRIC_UNSPEC) + g_string_append_printf(s, "Metric=%d\n", r->metric); + if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) + g_string_append_printf(s, "Table=%d\n", r->table); + if (r->mtubytes != NETPLAN_MTU_UNSPEC) + g_string_append_printf(s, "MTUBytes=%u\n", r->mtubytes); + if (r->congestion_window != NETPLAN_CONGESTION_WINDOW_UNSPEC) + g_string_append_printf(s, "InitialCongestionWindow=%u\n", r->congestion_window); + if (r->advertised_receive_window != NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC) + g_string_append_printf(s, "InitialAdvertisedReceiveWindow=%u\n", r->advertised_receive_window); +} + +static void +write_ip_rule(NetplanIPRule* r, GString* s) +{ + g_string_append_printf(s, "\n[RoutingPolicyRule]\n"); + + if (r->from) + g_string_append_printf(s, "From=%s\n", r->from); + if (r->to) + g_string_append_printf(s, "To=%s\n", r->to); + + if (r->table != NETPLAN_ROUTE_TABLE_UNSPEC) + g_string_append_printf(s, "Table=%d\n", r->table); + if (r->priority != NETPLAN_IP_RULE_PRIO_UNSPEC) + g_string_append_printf(s, "Priority=%d\n", r->priority); + if (r->fwmark != NETPLAN_IP_RULE_FW_MARK_UNSPEC) + g_string_append_printf(s, "FirewallMark=%d\n", r->fwmark); + if (r->tos != NETPLAN_IP_RULE_TOS_UNSPEC) + g_string_append_printf(s, "TypeOfService=%d\n", r->tos); +} + +static void +write_addr_option(NetplanAddressOptions* o, GString* s) +{ + g_string_append_printf(s, "\n[Address]\n"); + g_assert(o->address); + g_string_append_printf(s, "Address=%s\n", o->address); + + if (o->lifetime) + g_string_append_printf(s, "PreferredLifetime=%s\n", o->lifetime); + if (o->label) + g_string_append_printf(s, "Label=%s\n", o->label); +} + +#define DHCP_OVERRIDES_ERROR \ + "ERROR: %s: networkd requires that %s has the same value in both " \ + "dhcp4_overrides and dhcp6_overrides\n" + +static void +combine_dhcp_overrides(const NetplanNetDefinition* def, NetplanDHCPOverrides* combined_dhcp_overrides) +{ + /* if only one of dhcp4 or dhcp6 is enabled, those overrides are used */ + if (def->dhcp4 && !def->dhcp6) { + *combined_dhcp_overrides = def->dhcp4_overrides; + } else if (!def->dhcp4 && def->dhcp6) { + *combined_dhcp_overrides = def->dhcp6_overrides; + } else { + /* networkd doesn't support separately configuring dhcp4 and dhcp6, so + * we enforce that they are the same. + */ + if (def->dhcp4_overrides.use_dns != def->dhcp6_overrides.use_dns) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-dns"); + exit(1); + } + if (g_strcmp0(def->dhcp4_overrides.use_domains, def->dhcp6_overrides.use_domains) != 0){ + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-domains"); + exit(1); + } + if (def->dhcp4_overrides.use_ntp != def->dhcp6_overrides.use_ntp) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-ntp"); + exit(1); + } + if (def->dhcp4_overrides.send_hostname != def->dhcp6_overrides.send_hostname) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "send-hostname"); + exit(1); + } + if (def->dhcp4_overrides.use_hostname != def->dhcp6_overrides.use_hostname) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-hostname"); + exit(1); + } + if (def->dhcp4_overrides.use_mtu != def->dhcp6_overrides.use_mtu) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-mtu"); + exit(1); + } + if (g_strcmp0(def->dhcp4_overrides.hostname, def->dhcp6_overrides.hostname) != 0) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "hostname"); + exit(1); + } + if (def->dhcp4_overrides.metric != def->dhcp6_overrides.metric) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "route-metric"); + exit(1); + } + if (def->dhcp4_overrides.use_routes != def->dhcp6_overrides.use_routes) { + g_fprintf(stderr, DHCP_OVERRIDES_ERROR, def->id, "use-routes"); + exit(1); + } + /* Just use dhcp4_overrides now, since we know they are the same. */ + *combined_dhcp_overrides = def->dhcp4_overrides; + } +} + +/** + * Write the needed networkd .network configuration for the selected netplan definition. + */ +void +write_network_file(const NetplanNetDefinition* def, const char* rootdir, const char* path) +{ + GString* network = NULL; + GString* link = NULL; + GString* s = NULL; + mode_t orig_umask; + gboolean is_optional = def->optional; + + if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) { + g_debug("%s is defined as a hardware SR-IOV filtered VLAN, postponing creation", def->id); + return; + } + + /* Prepare the [Link] section of the .network file. */ + link = g_string_sized_new(200); + + /* Prepare the [Network] section */ + network = g_string_sized_new(200); + + /* The ActivationPolicy setting is available in systemd v248+ */ + if (def->activation_mode) { + const char* mode; + if (g_strcmp0(def->activation_mode, "manual") == 0) + mode = "manual"; + else /* "off" */ + mode = "always-down"; + g_string_append_printf(link, "ActivationPolicy=%s\n", mode); + /* When activation-mode is used we default to being optional. + * Otherwise systemd might wait indefinitely for the interface to + * become online. + */ + is_optional = TRUE; + } + + if (is_optional || def->optional_addresses) { + if (is_optional) { + g_string_append(link, "RequiredForOnline=no\n"); + } + for (unsigned i = 0; NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name != NULL; ++i) { + if (def->optional_addresses & NETPLAN_OPTIONAL_ADDRESS_TYPES[i].flag) { + g_string_append_printf(link, "OptionalAddresses=%s\n", NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name); + } + } + } + + if (def->mtubytes) + g_string_append_printf(link, "MTUBytes=%u\n", def->mtubytes); + if (def->set_mac) + g_string_append_printf(link, "MACAddress=%s\n", def->set_mac); + + if (def->emit_lldp) + g_string_append(network, "EmitLLDP=true\n"); + + if (def->dhcp4 && def->dhcp6) + g_string_append(network, "DHCP=yes\n"); + else if (def->dhcp4) + g_string_append(network, "DHCP=ipv4\n"); + else if (def->dhcp6) + g_string_append(network, "DHCP=ipv6\n"); + + /* Set link local addressing -- this does not apply to bond and bridge + * member interfaces, which always get it disabled. + */ + if (!def->bond && !def->bridge && (def->linklocal.ipv4 || def->linklocal.ipv6)) { + if (def->linklocal.ipv4 && def->linklocal.ipv6) + g_string_append(network, "LinkLocalAddressing=yes\n"); + else if (def->linklocal.ipv4) + g_string_append(network, "LinkLocalAddressing=ipv4\n"); + else if (def->linklocal.ipv6) + g_string_append(network, "LinkLocalAddressing=ipv6\n"); + } else { + g_string_append(network, "LinkLocalAddressing=no\n"); + } + + if (def->ip4_addresses) + for (unsigned i = 0; i < def->ip4_addresses->len; ++i) + g_string_append_printf(network, "Address=%s\n", g_array_index(def->ip4_addresses, char*, i)); + if (def->ip6_addresses) + for (unsigned i = 0; i < def->ip6_addresses->len; ++i) + g_string_append_printf(network, "Address=%s\n", g_array_index(def->ip6_addresses, char*, i)); + if (def->ip6_addr_gen_token) { + g_string_append_printf(network, "IPv6Token=static:%s\n", def->ip6_addr_gen_token); + } else if (def->ip6_addr_gen_mode > NETPLAN_ADDRGEN_EUI64) { + /* EUI-64 mode is enabled by default, if no IPv6Token= is specified */ + /* TODO: Enable stable-privacy mode for networkd, once PR#16618 has been released: + * https://github.com/systemd/systemd/pull/16618 */ + g_fprintf(stderr, "ERROR: %s: ipv6-address-generation mode is not supported by networkd\n", def->id); + exit(1); + } + if (def->accept_ra == NETPLAN_RA_MODE_ENABLED) + g_string_append_printf(network, "IPv6AcceptRA=yes\n"); + else if (def->accept_ra == NETPLAN_RA_MODE_DISABLED) + g_string_append_printf(network, "IPv6AcceptRA=no\n"); + if (def->ip6_privacy) + g_string_append(network, "IPv6PrivacyExtensions=yes\n"); + if (def->gateway4) + g_string_append_printf(network, "Gateway=%s\n", def->gateway4); + if (def->gateway6) + g_string_append_printf(network, "Gateway=%s\n", def->gateway6); + if (def->ip4_nameservers) + for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) + g_string_append_printf(network, "DNS=%s\n", g_array_index(def->ip4_nameservers, char*, i)); + if (def->ip6_nameservers) + for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) + g_string_append_printf(network, "DNS=%s\n", g_array_index(def->ip6_nameservers, char*, i)); + if (def->search_domains) { + g_string_append_printf(network, "Domains=%s", g_array_index(def->search_domains, char*, 0)); + for (unsigned i = 1; i < def->search_domains->len; ++i) + g_string_append_printf(network, " %s", g_array_index(def->search_domains, char*, i)); + g_string_append(network, "\n"); + } + + if (def->ipv6_mtubytes) { + g_string_append_printf(network, "IPv6MTUBytes=%d\n", def->ipv6_mtubytes); + } + + if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL) + g_string_append(network, "ConfigureWithoutCarrier=yes\n"); + + if (def->bridge && def->backend != NETPLAN_BACKEND_OVS) { + g_string_append_printf(network, "Bridge=%s\n", def->bridge); + + if (def->bridge_params.path_cost || def->bridge_params.port_priority) + g_string_append_printf(network, "\n[Bridge]\n"); + if (def->bridge_params.path_cost) + g_string_append_printf(network, "Cost=%u\n", def->bridge_params.path_cost); + if (def->bridge_params.port_priority) + g_string_append_printf(network, "Priority=%u\n", def->bridge_params.port_priority); + } + if (def->bond && def->backend != NETPLAN_BACKEND_OVS) { + g_string_append_printf(network, "Bond=%s\n", def->bond); + + if (def->bond_params.primary_slave) + g_string_append_printf(network, "PrimarySlave=true\n"); + } + + if (def->has_vlans && def->backend != NETPLAN_BACKEND_OVS) { + /* iterate over all netdefs to find VLANs attached to us */ + GList *l = netdefs_ordered; + const NetplanNetDefinition* nd; + for (; l != NULL; l = l->next) { + nd = l->data; + if (nd->vlan_link == def && !nd->sriov_vlan_filter) + g_string_append_printf(network, "VLAN=%s\n", nd->id); + } + } + + if (def->routes != NULL) { + for (unsigned i = 0; i < def->routes->len; ++i) { + NetplanIPRoute* cur_route = g_array_index (def->routes, NetplanIPRoute*, i); + write_route(cur_route, network); + } + } + if (def->ip_rules != NULL) { + for (unsigned i = 0; i < def->ip_rules->len; ++i) { + NetplanIPRule* cur_rule = g_array_index (def->ip_rules, NetplanIPRule*, i); + write_ip_rule(cur_rule, network); + } + } + + if (def->address_options) { + for (unsigned i = 0; i < def->address_options->len; ++i) { + NetplanAddressOptions* opts = g_array_index(def->address_options, NetplanAddressOptions*, i); + write_addr_option(opts, network); + } + } + + if (def->dhcp4 || def->dhcp6 || def->critical) { + /* NetworkManager compatible route metrics */ + g_string_append(network, "\n[DHCP]\n"); + } + + if (def->critical) + g_string_append_printf(network, "CriticalConnection=true\n"); + + if (def->dhcp4 || def->dhcp6) { + if (g_strcmp0(def->dhcp_identifier, "duid") != 0) + g_string_append_printf(network, "ClientIdentifier=%s\n", def->dhcp_identifier); + + NetplanDHCPOverrides combined_dhcp_overrides; + combine_dhcp_overrides(def, &combined_dhcp_overrides); + + if (combined_dhcp_overrides.metric == NETPLAN_METRIC_UNSPEC) { + g_string_append_printf(network, "RouteMetric=%i\n", (def->type == NETPLAN_DEF_TYPE_WIFI ? 600 : 100)); + } else { + g_string_append_printf(network, "RouteMetric=%u\n", + combined_dhcp_overrides.metric); + } + + /* Only set MTU from DHCP if use-mtu dhcp-override is not false. */ + if (!combined_dhcp_overrides.use_mtu) { + /* isc-dhcp dhclient compatible UseMTU, networkd default is to + * not accept MTU, which breaks clouds */ + g_string_append_printf(network, "UseMTU=false\n"); + } else { + g_string_append_printf(network, "UseMTU=true\n"); + } + + /* Only write DHCP options that differ from the networkd default. */ + if (!combined_dhcp_overrides.use_routes) + g_string_append_printf(network, "UseRoutes=false\n"); + if (!combined_dhcp_overrides.use_dns) + g_string_append_printf(network, "UseDNS=false\n"); + if (combined_dhcp_overrides.use_domains) + g_string_append_printf(network, "UseDomains=%s\n", combined_dhcp_overrides.use_domains); + if (!combined_dhcp_overrides.use_ntp) + g_string_append_printf(network, "UseNTP=false\n"); + if (!combined_dhcp_overrides.send_hostname) + g_string_append_printf(network, "SendHostname=false\n"); + if (!combined_dhcp_overrides.use_hostname) + g_string_append_printf(network, "UseHostname=false\n"); + if (combined_dhcp_overrides.hostname) + g_string_append_printf(network, "Hostname=%s\n", combined_dhcp_overrides.hostname); + } + + if (network->len > 0 || link->len > 0) { + s = g_string_sized_new(200); + append_match_section(def, s, TRUE); + + if (link->len > 0) + g_string_append_printf(s, "\n[Link]\n%s", link->str); + if (network->len > 0) + g_string_append_printf(s, "\n[Network]\n%s", network->str); + + g_string_free(link, TRUE); + g_string_free(network, TRUE); + + /* these do not contain secrets and need to be readable by + * systemd-networkd - LP: #1736965 */ + orig_umask = umask(022); + g_string_free_to_file(s, rootdir, path, ".network"); + umask(orig_umask); + } +} + +static void +write_rules_file(const NetplanNetDefinition* def, const char* rootdir) +{ + GString* s = NULL; + g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL); + mode_t orig_umask; + + /* do we need to write a .rules file? + * It's only required for reliably setting the name of a physical device + * until systemd issue #9006 is resolved. */ + if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL) + return; + + /* Matching by name does not work. + * + * As far as I can tell, if you match by the name coming out of + * initrd, systemd complains that a link file is matching on a + * renamed name. If you match by the unstable kernel name, the + * device no longer has that name when udevd reads the file, so + * the rule doesn't fire. So only support mac and driver. */ + if (!def->set_name || (!def->match.mac && !def->match.driver)) + return; + + /* build file contents */ + s = g_string_sized_new(200); + + g_string_append(s, "SUBSYSTEM==\"net\", ACTION==\"add\", "); + + if (def->match.driver) { + g_string_append_printf(s,"DRIVERS==\"%s\", ", def->match.driver); + } else { + g_string_append(s, "DRIVERS==\"?*\", "); + } + + if (def->match.mac) + g_string_append_printf(s, "ATTR{address}==\"%s\", ", def->match.mac); + + g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name); + + orig_umask = umask(022); + g_string_free_to_file(s, rootdir, path, NULL); + umask(orig_umask); +} + +static void +append_wpa_auth_conf(GString* s, const NetplanAuthenticationSettings* auth, const char* id) +{ + switch (auth->key_management) { + case NETPLAN_AUTH_KEY_MANAGEMENT_NONE: + g_string_append(s, " key_mgmt=NONE\n"); + break; + + case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK: + g_string_append(s, " key_mgmt=WPA-PSK\n"); + break; + + case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP: + g_string_append(s, " key_mgmt=WPA-EAP\n"); + break; + + case NETPLAN_AUTH_KEY_MANAGEMENT_8021X: + g_string_append(s, " key_mgmt=IEEE8021X\n"); + break; + + default: break; // LCOV_EXCL_LINE + } + + switch (auth->eap_method) { + case NETPLAN_AUTH_EAP_NONE: + break; + + case NETPLAN_AUTH_EAP_TLS: + g_string_append(s, " eap=TLS\n"); + break; + + case NETPLAN_AUTH_EAP_PEAP: + g_string_append(s, " eap=PEAP\n"); + break; + + case NETPLAN_AUTH_EAP_TTLS: + g_string_append(s, " eap=TTLS\n"); + break; + + default: break; // LCOV_EXCL_LINE + } + + if (auth->identity) { + g_string_append_printf(s, " identity=\"%s\"\n", auth->identity); + } + if (auth->anonymous_identity) { + g_string_append_printf(s, " anonymous_identity=\"%s\"\n", auth->anonymous_identity); + } + if (auth->password) { + if (auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK) { + size_t len = strlen(auth->password); + if (len == 64) { + /* must be a hex-digit key representation */ + for (unsigned i = 0; i < 64; ++i) + if (!isxdigit(auth->password[i])) { + g_fprintf(stderr, "ERROR: %s: PSK length of 64 is only supported for hex-digit representation\n", id); + exit(1); + } + /* this is required to be unquoted */ + g_string_append_printf(s, " psk=%s\n", auth->password); + } else if (len < 8 || len > 63) { + /* per wpa_supplicant spec, passphrase needs to be between 8 + and 63 characters */ + g_fprintf(stderr, "ERROR: %s: ASCII passphrase must be between 8 and 63 characters (inclusive)\n", id); + exit(1); + } else { + g_string_append_printf(s, " psk=\"%s\"\n", auth->password); + } + } else { + if (strncmp(auth->password, "hash:", 5) == 0) { + g_string_append_printf(s, " password=%s\n", auth->password); + } else { + g_string_append_printf(s, " password=\"%s\"\n", auth->password); + } + } + } + if (auth->ca_certificate) { + g_string_append_printf(s, " ca_cert=\"%s\"\n", auth->ca_certificate); + } + if (auth->client_certificate) { + g_string_append_printf(s, " client_cert=\"%s\"\n", auth->client_certificate); + } + if (auth->client_key) { + g_string_append_printf(s, " private_key=\"%s\"\n", auth->client_key); + } + if (auth->client_key_password) { + g_string_append_printf(s, " private_key_passwd=\"%s\"\n", auth->client_key_password); + } + if (auth->phase2_auth) { + g_string_append_printf(s, " phase2=\"auth=%s\"\n", auth->phase2_auth); + } + +} + +/* netplan-feature: generated-supplicant */ +static void +write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir) +{ + g_autofree gchar *stdouth = NULL; + + stdouth = systemd_escape(def->id); + + GString* s = g_string_new("[Unit]\n"); + g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", stdouth, ".service", NULL); + g_string_append_printf(s, "Description=WPA supplicant for netplan %s\n", stdouth); + g_string_append(s, "DefaultDependencies=no\n"); + g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", stdouth); + g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", stdouth); + g_string_append(s, "Before=network.target\nWants=network.target\n\n"); + g_string_append(s, "[Service]\nType=simple\n"); + g_string_append_printf(s, "ExecStart=/sbin/wpa_supplicant -c /run/netplan/wpa-%s.conf -i%s", stdouth, stdouth); + + if (def->type != NETPLAN_DEF_TYPE_WIFI) { + g_string_append(s, " -Dwired\n"); + } + g_string_free_to_file(s, rootdir, path, NULL); +} + +static void +write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir) +{ + GHashTableIter iter; + GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n"); + g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL); + mode_t orig_umask; + + g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path); + if (def->type == NETPLAN_DEF_TYPE_WIFI) { + if (def->wowlan && def->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT) { + g_string_append(s, "wowlan_triggers="); + append_wifi_wowlan_flags(def->wowlan, s); + } + NetplanWifiAccessPoint* ap; + g_hash_table_iter_init(&iter, def->access_points); + while (g_hash_table_iter_next(&iter, NULL, (gpointer) &ap)) { + g_string_append_printf(s, "network={\n ssid=\"%s\"\n", ap->ssid); + if (ap->bssid) { + g_string_append_printf(s, " bssid=%s\n", ap->bssid); + } + if (ap->hidden) { + g_string_append(s, " scan_ssid=1\n"); + } + if (ap->band == NETPLAN_WIFI_BAND_24) { + // initialize 2.4GHz frequency hashtable + if(!wifi_frequency_24) + wifi_get_freq24(1); + if (ap->channel) { + g_string_append_printf(s, " freq_list=%d\n", wifi_get_freq24(ap->channel)); + } else { + g_string_append_printf(s, " freq_list="); + g_hash_table_foreach(wifi_frequency_24, wifi_append_freq, s); + // overwrite last whitespace with newline + s = g_string_overwrite(s, s->len-1, "\n"); + } + } else if (ap->band == NETPLAN_WIFI_BAND_5) { + // initialize 5GHz frequency hashtable + if(!wifi_frequency_5) + wifi_get_freq5(7); + if (ap->channel) { + g_string_append_printf(s, " freq_list=%d\n", wifi_get_freq5(ap->channel)); + } else { + g_string_append_printf(s, " freq_list="); + g_hash_table_foreach(wifi_frequency_5, wifi_append_freq, s); + // overwrite last whitespace with newline + s = g_string_overwrite(s, s->len-1, "\n"); + } + } + switch (ap->mode) { + case NETPLAN_WIFI_MODE_INFRASTRUCTURE: + /* default in wpasupplicant */ + break; + case NETPLAN_WIFI_MODE_ADHOC: + g_string_append(s, " mode=1\n"); + break; + default: + g_fprintf(stderr, "ERROR: %s: %s: networkd does not support this wifi mode\n", def->id, ap->ssid); + exit(1); + } + + /* wifi auth trumps netdef auth */ + if (ap->has_auth) { + append_wpa_auth_conf(s, &ap->auth, ap->ssid); + } + else { + g_string_append(s, " key_mgmt=NONE\n"); + } + g_string_append(s, "}\n"); + } + } + else { + /* wired 802.1x auth or similar */ + g_string_append(s, "network={\n"); + append_wpa_auth_conf(s, &def->auth, def->id); + g_string_append(s, "}\n"); + } + + /* use tight permissions as this contains secrets */ + orig_umask = umask(077); + g_string_free_to_file(s, rootdir, path, NULL); + umask(orig_umask); +} + +/** + * Generate networkd configuration in @rootdir/run/systemd/network/ from the + * parsed #netdefs. + * @rootdir: If not %NULL, generate configuration in this root directory + * (useful for testing). + * Returns: TRUE if @def applies to networkd, FALSE otherwise. + */ +gboolean +write_networkd_conf(const NetplanNetDefinition* def, const char* rootdir) +{ + g_autofree char* path_base = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL); + + /* We want this for all backends when renaming, as *.link and *.rules files are + * evaluated by udev, not networkd itself or NetworkManager. */ + write_link_file(def, rootdir, path_base); + write_rules_file(def, rootdir); + + if (def->backend != NETPLAN_BACKEND_NETWORKD) { + g_debug("networkd: definition %s is not for us (backend %i)", def->id, def->backend); + return FALSE; + } + + if (def->type == NETPLAN_DEF_TYPE_MODEM) { + g_fprintf(stderr, "ERROR: %s: networkd backend does not support GSM/CDMA modem configuration\n", def->id); + exit(1); + } + + if (def->type == NETPLAN_DEF_TYPE_WIFI || def->has_auth) { + g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-", def->id, ".service", NULL); + g_autofree char* slink = g_strjoin(NULL, "/run/systemd/system/netplan-wpa-", def->id, ".service", NULL); + if (def->type == NETPLAN_DEF_TYPE_WIFI && def->has_match) { + g_fprintf(stderr, "ERROR: %s: networkd backend does not support wifi with match:, only by interface name\n", def->id); + exit(1); + } + + g_debug("Creating wpa_supplicant config"); + write_wpa_conf(def, rootdir); + + g_debug("Creating wpa_supplicant unit %s", slink); + write_wpa_unit(def, rootdir); + + g_debug("Creating wpa_supplicant service enablement link %s", link); + safe_mkdir_p_dir(link); + + if (symlink(slink, link) < 0 && errno != EEXIST) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to create enablement symlink: %m\n"); + exit(1); + // LCOV_EXCL_STOP + } + + } + + if (def->type >= NETPLAN_DEF_TYPE_VIRTUAL) + write_netdev_file(def, rootdir, path_base); + write_network_file(def, rootdir, path_base); + return TRUE; +} + +/** + * Clean up all generated configurations in @rootdir from previous runs. + */ +void +cleanup_networkd_conf(const char* rootdir) +{ + unlink_glob(rootdir, "/run/systemd/network/10-netplan-*"); + unlink_glob(rootdir, "/run/netplan/wpa-*.conf"); + unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa-*.service"); + unlink_glob(rootdir, "/run/systemd/system/netplan-wpa-*.service"); + unlink_glob(rootdir, "/run/udev/rules.d/99-netplan-*"); + /* Historically (up to v0.98) we had netplan-wpa@*.service files, in case of an + * upgraded system, we need to make sure to clean those up. */ + unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-wpa@*.service"); +} + +/** + * Create enablement symlink for systemd-networkd.service. + */ +void +enable_networkd(const char* generator_dir) +{ + g_autofree char* link = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "multi-user.target.wants", "systemd-networkd.service", NULL); + g_debug("We created networkd configuration, adding %s enablement symlink", link); + safe_mkdir_p_dir(link); + if (symlink("../systemd-networkd.service", link) < 0 && errno != EEXIST) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to create enablement symlink: %m\n"); + exit(1); + // LCOV_EXCL_STOP + } + + g_autofree char* link2 = g_build_path(G_DIR_SEPARATOR_S, generator_dir, "network-online.target.wants", "systemd-networkd-wait-online.service", NULL); + safe_mkdir_p_dir(link2); + if (symlink("/lib/systemd/system/systemd-networkd-wait-online.service", link2) < 0 && errno != EEXIST) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to create enablement symlink: %m\n"); + exit(1); + // LCOV_EXCL_STOP + } +} diff --git a/src/networkd.h b/src/networkd.h new file mode 100644 index 0000000..41ab125 --- /dev/null +++ b/src/networkd.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2016 Canonical, Ltd. + * Author: Martin Pitt + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "parse.h" + +gboolean write_networkd_conf(const NetplanNetDefinition* def, const char* rootdir); +void cleanup_networkd_conf(const char* rootdir); +void enable_networkd(const char* generator_dir); + +void write_network_file(const NetplanNetDefinition* def, const char* rootdir, const char* path); diff --git a/src/nm.c b/src/nm.c new file mode 100644 index 0000000..aef15ac --- /dev/null +++ b/src/nm.c @@ -0,0 +1,999 @@ +/* + * Copyright (C) 2016-2021 Canonical, Ltd. + * Author: Martin Pitt + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "nm.h" +#include "parse.h" +#include "util.h" +#include "validation.h" +#include "parse-nm.h" + +GString* udev_rules; + +/** + * Append NM device specifier of @def to @s. + */ +static void +g_string_append_netdef_match(GString* s, const NetplanNetDefinition* def) +{ + g_assert(!def->match.driver || def->set_name); + if (def->match.mac || def->match.original_name || def->set_name || def->type >= NETPLAN_DEF_TYPE_VIRTUAL) { + if (def->match.mac) { + g_string_append_printf(s, "mac:%s,", def->match.mac); + } + /* MAC could change, e.g. for bond slaves. Ignore by interface-name as well */ + if (def->match.original_name || def->set_name || def->type >= NETPLAN_DEF_TYPE_VIRTUAL) { + /* we always have the renamed name here */ + g_string_append_printf(s, "interface-name:%s,", + (def->type >= NETPLAN_DEF_TYPE_VIRTUAL) ? def->id + : (def->set_name ?: def->match.original_name)); + } + } else { + /* no matches → match all devices of that type */ + switch (def->type) { + case NETPLAN_DEF_TYPE_ETHERNET: + g_string_append(s, "type:ethernet,"); + break; + /* This cannot be reached with just NM and networkd backends, as + * networkd does not support wifi and thus we'll never blacklist a + * wifi device from NM. This would become relevant with another + * wifi-supporting backend, but until then this just spoils 100% + * code coverage. + case NETPLAN_DEF_TYPE_WIFI: + g_string_append(s, "type:wifi"); + break; + */ + + // LCOV_EXCL_START + default: + g_assert_not_reached(); + // LCOV_EXCL_STOP + } + } +} + +/** + * Infer if this is a modem netdef of type GSM. + * This is done by checking for certain modem_params, which are only + * applicable to GSM connections. + */ +static const gboolean +modem_is_gsm(const NetplanNetDefinition* def) +{ + if ( def->modem_params.apn + || def->modem_params.auto_config + || def->modem_params.device_id + || def->modem_params.network_id + || def->modem_params.pin + || def->modem_params.sim_id + || def->modem_params.sim_operator_id) + return TRUE; + + return FALSE; +} + +/** + * Return NM "type=" string. + */ +static const char* +type_str(const NetplanNetDefinition* def) +{ + const NetplanDefType type = def->type; + switch (type) { + case NETPLAN_DEF_TYPE_ETHERNET: + return "ethernet"; + case NETPLAN_DEF_TYPE_MODEM: + if (modem_is_gsm(def)) + return "gsm"; + else + return "cdma"; + case NETPLAN_DEF_TYPE_WIFI: + return "wifi"; + case NETPLAN_DEF_TYPE_BRIDGE: + return "bridge"; + case NETPLAN_DEF_TYPE_BOND: + return "bond"; + case NETPLAN_DEF_TYPE_VLAN: + return "vlan"; + case NETPLAN_DEF_TYPE_TUNNEL: + if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) + return "wireguard"; + return "ip-tunnel"; + case NETPLAN_DEF_TYPE_NM: + /* needs to be overriden by passthrough "connection.type" setting */ + return NULL; + // LCOV_EXCL_START + default: + g_assert_not_reached(); + // LCOV_EXCL_STOP + } +} + +/** + * Return NM wifi "mode=" string. + */ +static const char* +wifi_mode_str(const NetplanWifiMode mode) +{ + switch (mode) { + case NETPLAN_WIFI_MODE_INFRASTRUCTURE: + return "infrastructure"; + case NETPLAN_WIFI_MODE_ADHOC: + return "adhoc"; + case NETPLAN_WIFI_MODE_AP: + return "ap"; + // LCOV_EXCL_START + default: + g_assert_not_reached(); + // LCOV_EXCL_STOP + } +} + +/** + * Return NM wifi "band=" string. + */ +static const char* +wifi_band_str(const NetplanWifiBand band) +{ + switch (band) { + case NETPLAN_WIFI_BAND_5: + return "a"; + case NETPLAN_WIFI_BAND_24: + return "bg"; + // LCOV_EXCL_START + default: + g_assert_not_reached(); + // LCOV_EXCL_STOP + } +} + +/** + * Return NM addr-gen-mode string. + */ +static const char* +addr_gen_mode_str(const NetplanAddrGenMode mode) +{ + switch (mode) { + case NETPLAN_ADDRGEN_EUI64: + return "0"; + case NETPLAN_ADDRGEN_STABLEPRIVACY: + return "1"; + // LCOV_EXCL_START + default: + g_assert_not_reached(); + // LCOV_EXCL_STOP + } +} + +static void +write_search_domains(const NetplanNetDefinition* def, const char* group, GKeyFile *kf) +{ + if (def->search_domains) { + const gchar* list[def->search_domains->len]; + for (unsigned i = 0; i < def->search_domains->len; ++i) + list[i] = g_array_index(def->search_domains, char*, i); + g_key_file_set_string_list(kf, group, "dns-search", list, def->search_domains->len); + } +} + +static void +write_routes(const NetplanNetDefinition* def, GKeyFile *kf, int family) +{ + const gchar* group = NULL; + gchar* tmp_key = NULL; + GString* tmp_val = NULL; + + if (family == AF_INET) + group = "ipv4"; + else if (family == AF_INET6) + group = "ipv6"; + g_assert(group != NULL); + + if (def->routes != NULL) { + for (unsigned i = 0, j = 1; i < def->routes->len; ++i) { + const NetplanIPRoute *cur_route = g_array_index(def->routes, NetplanIPRoute*, i); + const char *destination; + + if (cur_route->family != family) + continue; + + if (g_strcmp0(cur_route->to, "default") == 0) + destination = get_global_network(family); + else + destination = cur_route->to; + + if (cur_route->type && g_ascii_strcasecmp(cur_route->type, "unicast") != 0) { + g_fprintf(stderr, "ERROR: %s: NetworkManager only supports unicast routes\n", def->id); + exit(1); + } + + if (!g_strcmp0(cur_route->scope, "global")) { + /* For IPv6 addresses, kernel and NetworkManager don't support a scope. + * For IPv4 addresses, NetworkManager determines the scope of addresses on its own + * ("link" for addresses without gateway, "global" for addresses with next-hop). */ + g_debug("%s: NetworkManager does not support setting a scope for routes, it will auto-detect them.", def->id); + } else if (cur_route->scope) { + /* Error out if scope is not set to its default value of 'global' */ + g_fprintf(stderr, "ERROR: %s: NetworkManager does not support setting a scope for routes\n", def->id); + exit(1); + } + + tmp_key = g_strdup_printf("route%d", j); + tmp_val = g_string_new(NULL); + g_string_printf(tmp_val, "%s,%s", destination, cur_route->via); + if (cur_route->metric != NETPLAN_METRIC_UNSPEC) + g_string_append_printf(tmp_val, ",%d", cur_route->metric); + g_key_file_set_string(kf, group, tmp_key, tmp_val->str); + g_free(tmp_key); + g_string_free(tmp_val, TRUE); + + if ( cur_route->onlink + || cur_route->advertised_receive_window + || cur_route->congestion_window + || cur_route->mtubytes + || cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC + || cur_route->from) { + tmp_key = g_strdup_printf("route%d_options", j); + tmp_val = g_string_new(NULL); + if (cur_route->onlink) { + /* onlink for IPv6 addresses is only supported since nm-1.18.0. */ + g_string_append_printf(tmp_val, "onlink=true,"); + } + if (cur_route->advertised_receive_window != NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC) + g_string_append_printf(tmp_val, "initrwnd=%u,", cur_route->advertised_receive_window); + if (cur_route->congestion_window != NETPLAN_CONGESTION_WINDOW_UNSPEC) + g_string_append_printf(tmp_val, "initcwnd=%u,", cur_route->congestion_window); + if (cur_route->mtubytes != NETPLAN_MTU_UNSPEC) + g_string_append_printf(tmp_val, "mtu=%u,", cur_route->mtubytes); + if (cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC) + g_string_append_printf(tmp_val, "table=%u,", cur_route->table); + if (cur_route->from) + g_string_append_printf(tmp_val, "src=%s,", cur_route->from); + tmp_val->str[tmp_val->len - 1] = '\0'; //remove trailing comma + g_key_file_set_string(kf, group, tmp_key, tmp_val->str); + g_free(tmp_key); + g_string_free(tmp_val, TRUE); + } + j++; + } + } +} + +static void +write_bond_parameters(const NetplanNetDefinition* def, GKeyFile *kf) +{ + GString* tmp_val = NULL; + if (def->bond_params.mode) + g_key_file_set_string(kf, "bond", "mode", def->bond_params.mode); + if (def->bond_params.lacp_rate) + g_key_file_set_string(kf, "bond", "lacp_rate", def->bond_params.lacp_rate); + if (def->bond_params.monitor_interval) + g_key_file_set_string(kf, "bond", "miimon", def->bond_params.monitor_interval); + if (def->bond_params.min_links) + g_key_file_set_integer(kf, "bond", "min_links", def->bond_params.min_links); + if (def->bond_params.transmit_hash_policy) + g_key_file_set_string(kf, "bond", "xmit_hash_policy", def->bond_params.transmit_hash_policy); + if (def->bond_params.selection_logic) + g_key_file_set_string(kf, "bond", "ad_select", def->bond_params.selection_logic); + if (def->bond_params.all_slaves_active) + g_key_file_set_integer(kf, "bond", "all_slaves_active", def->bond_params.all_slaves_active); + if (def->bond_params.arp_interval) + g_key_file_set_string(kf, "bond", "arp_interval", def->bond_params.arp_interval); + if (def->bond_params.arp_ip_targets) { + tmp_val = g_string_new(NULL); + for (unsigned i = 0; i < def->bond_params.arp_ip_targets->len; ++i) { + if (i > 0) + g_string_append_printf(tmp_val, ","); + g_string_append_printf(tmp_val, "%s", g_array_index(def->bond_params.arp_ip_targets, char*, i)); + } + g_key_file_set_string(kf, "bond", "arp_ip_target", tmp_val->str); + g_string_free(tmp_val, TRUE); + } + if (def->bond_params.arp_validate) + g_key_file_set_string(kf, "bond", "arp_validate", def->bond_params.arp_validate); + if (def->bond_params.arp_all_targets) + g_key_file_set_string(kf, "bond", "arp_all_targets", def->bond_params.arp_all_targets); + if (def->bond_params.up_delay) + g_key_file_set_string(kf, "bond", "updelay", def->bond_params.up_delay); + if (def->bond_params.down_delay) + g_key_file_set_string(kf, "bond", "downdelay", def->bond_params.down_delay); + if (def->bond_params.fail_over_mac_policy) + g_key_file_set_string(kf, "bond", "fail_over_mac", def->bond_params.fail_over_mac_policy); + if (def->bond_params.gratuitous_arp) { + g_key_file_set_integer(kf, "bond", "num_grat_arp", def->bond_params.gratuitous_arp); + /* Work around issue in NM where unset unsolicited_na will overwrite num_grat_arp: + * https://github.com/NetworkManager/NetworkManager/commit/42b0bef33c77a0921590b2697f077e8ea7805166 */ + g_key_file_set_integer(kf, "bond", "num_unsol_na", def->bond_params.gratuitous_arp); + } + if (def->bond_params.packets_per_slave) + g_key_file_set_integer(kf, "bond", "packets_per_slave", def->bond_params.packets_per_slave); + if (def->bond_params.primary_reselect_policy) + g_key_file_set_string(kf, "bond", "primary_reselect", def->bond_params.primary_reselect_policy); + if (def->bond_params.resend_igmp) + g_key_file_set_integer(kf, "bond", "resend_igmp", def->bond_params.resend_igmp); + if (def->bond_params.learn_interval) + g_key_file_set_string(kf, "bond", "lp_interval", def->bond_params.learn_interval); + if (def->bond_params.primary_slave) + g_key_file_set_string(kf, "bond", "primary", def->bond_params.primary_slave); +} + +static void +write_bridge_params(const NetplanNetDefinition* def, GKeyFile *kf) +{ + if (def->custom_bridging) { + if (def->bridge_params.ageing_time) + g_key_file_set_string(kf, "bridge", "ageing-time", def->bridge_params.ageing_time); + if (def->bridge_params.priority) + g_key_file_set_uint64(kf, "bridge", "priority", def->bridge_params.priority); + if (def->bridge_params.forward_delay) + g_key_file_set_string(kf, "bridge", "forward-delay", def->bridge_params.forward_delay); + if (def->bridge_params.hello_time) + g_key_file_set_string(kf, "bridge", "hello-time", def->bridge_params.hello_time); + if (def->bridge_params.max_age) + g_key_file_set_string(kf, "bridge", "max-age", def->bridge_params.max_age); + g_key_file_set_boolean(kf, "bridge", "stp", def->bridge_params.stp); + } +} + +static void +write_wireguard_params(const NetplanNetDefinition* def, GKeyFile *kf) +{ + gchar* tmp_group = NULL; + g_assert(def->tunnel.private_key); + + /* The key was already validated via validate_tunnel_grammar(), but we need + * to differentiate between base64 key VS absolute path key-file. And a base64 + * string could (theoretically) start with '/', so we use is_wireguard_key() + * as well to check for more specific characteristics (if needed). */ + if (def->tunnel.private_key[0] == '/' && !is_wireguard_key(def->tunnel.private_key)) { + g_fprintf(stderr, "%s: private key needs to be base64 encoded when using the NM backend\n", def->id); + exit(1); + } else + g_key_file_set_string(kf, "wireguard", "private-key", def->tunnel.private_key); + + if (def->tunnel.port) + g_key_file_set_uint64(kf, "wireguard", "listen-port", def->tunnel.port); + if (def->tunnel.fwmark) + g_key_file_set_uint64(kf, "wireguard", "fwmark", def->tunnel.fwmark); + + for (guint i = 0; i < def->wireguard_peers->len; i++) { + NetplanWireguardPeer *peer = g_array_index (def->wireguard_peers, NetplanWireguardPeer*, i); + g_assert(peer->public_key); + tmp_group = g_strdup_printf("wireguard-peer.%s", peer->public_key); + + if (peer->keepalive) + g_key_file_set_integer(kf, tmp_group, "persistent-keepalive", peer->keepalive); + if (peer->endpoint) + g_key_file_set_string(kf, tmp_group, "endpoint", peer->endpoint); + + /* The key was already validated via validate_tunnel_grammar(), but we need + * to differentiate between base64 key VS absolute path key-file. And a base64 + * string could (theoretically) start with '/', so we use is_wireguard_key() + * as well to check for more specific characteristics (if needed). */ + if (peer->preshared_key) { + if (peer->preshared_key[0] == '/' && !is_wireguard_key(peer->preshared_key)) { + g_fprintf(stderr, "%s: shared key needs to be base64 encoded when using the NM backend\n", def->id); + exit(1); + } else { + g_key_file_set_value(kf, tmp_group, "preshared-key", peer->preshared_key); + g_key_file_set_uint64(kf, tmp_group, "preshared-key-flags", 0); + } + } + if (peer->allowed_ips && peer->allowed_ips->len > 0) { + const gchar* list[peer->allowed_ips->len]; + for (guint j = 0; j < peer->allowed_ips->len; ++j) + list[j] = g_array_index(peer->allowed_ips, char*, j); + g_key_file_set_string_list(kf, tmp_group, "allowed-ips", list, peer->allowed_ips->len); + } + g_free(tmp_group); + } +} + +static void +write_tunnel_params(const NetplanNetDefinition* def, GKeyFile *kf) +{ + g_key_file_set_integer(kf, "ip-tunnel", "mode", def->tunnel.mode); + g_key_file_set_string(kf, "ip-tunnel", "local", def->tunnel.local_ip); + g_key_file_set_string(kf, "ip-tunnel", "remote", def->tunnel.remote_ip); + if (def->tunnel_ttl) + g_key_file_set_uint64(kf, "ip-tunnel", "ttl", def->tunnel_ttl); + if (def->tunnel.input_key) + g_key_file_set_string(kf, "ip-tunnel", "input-key", def->tunnel.input_key); + if (def->tunnel.output_key) + g_key_file_set_string(kf, "ip-tunnel", "output-key", def->tunnel.output_key); +} + +static void +write_dot1x_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) +{ + if (auth->eap_method == NETPLAN_AUTH_EAP_NONE) + return; + + switch (auth->eap_method) { + case NETPLAN_AUTH_EAP_TLS: + g_key_file_set_string(kf, "802-1x", "eap", "tls"); + break; + case NETPLAN_AUTH_EAP_PEAP: + g_key_file_set_string(kf, "802-1x", "eap", "peap"); + break; + case NETPLAN_AUTH_EAP_TTLS: + g_key_file_set_string(kf, "802-1x", "eap", "ttls"); + break; + default: break; // LCOV_EXCL_LINE + } + + if (auth->identity) + g_key_file_set_string(kf, "802-1x", "identity", auth->identity); + if (auth->anonymous_identity) + g_key_file_set_string(kf, "802-1x", "anonymous-identity", auth->anonymous_identity); + if (auth->password && auth->key_management != NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK) + g_key_file_set_string(kf, "802-1x", "password", auth->password); + if (auth->ca_certificate) + g_key_file_set_string(kf, "802-1x", "ca-cert", auth->ca_certificate); + if (auth->client_certificate) + g_key_file_set_string(kf, "802-1x", "client-cert", auth->client_certificate); + if (auth->client_key) + g_key_file_set_string(kf, "802-1x", "private-key", auth->client_key); + if (auth->client_key_password) + g_key_file_set_string(kf, "802-1x", "private-key-password", auth->client_key_password); + if (auth->phase2_auth) + g_key_file_set_string(kf, "802-1x", "phase2-auth", auth->phase2_auth); +} + +static void +write_wifi_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) +{ + if (auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_NONE) + return; + + switch (auth->key_management) { + case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK: + g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-psk"); + if (auth->password) + g_key_file_set_string(kf, "wifi-security", "psk", auth->password); + break; + case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP: + g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-eap"); + break; + case NETPLAN_AUTH_KEY_MANAGEMENT_8021X: + g_key_file_set_string(kf, "wifi-security", "key-mgmt", "ieee8021x"); + break; + default: break; // LCOV_EXCL_LINE + } + + write_dot1x_auth_parameters(auth, kf); +} + +static void +maybe_generate_uuid(NetplanNetDefinition* def) +{ + if (uuid_is_null(def->uuid)) + uuid_generate(def->uuid); +} + +/** + * Special handling for passthrough mode: read key-value pairs from + * "backend_settings.nm.passthrough" and inject them into the keyfile as-is. + */ +static void +write_fallback_key_value(GQuark key_id, gpointer value, gpointer user_data) +{ + GKeyFile *kf = user_data; + gchar* val = value; + /* Group name may contain dots, but key name may not. + * The "tc" group is a special case, where it is the other way around, e.g.: + * tc->qdisc.root + * tc->tfilter.ffff: */ + const gchar* key = g_quark_to_string(key_id); + gchar **group_key = g_strsplit(key, ".", -1); + guint len = g_strv_length(group_key); + g_autofree gchar* old_key = NULL; + gboolean has_key = FALSE; + g_autofree gchar* k = NULL; + g_autofree gchar* group = NULL; + if (!g_strcmp0(group_key[0], "tc") && len > 2) { + k = g_strconcat(group_key[1], ".", group_key[2], NULL); + group = g_strdup(group_key[0]); + } else { + k = group_key[len-1]; + group_key[len-1] = NULL; //remove key from array + group = g_strjoinv(".", group_key); //re-combine group parts + } + + has_key = g_key_file_has_key(kf, group, k, NULL); + old_key = g_key_file_get_string(kf, group, k, NULL); + g_key_file_set_string(kf, group, k, val); + /* delete the dummy key, if this was just an empty group */ + if (!g_strcmp0(k, NETPLAN_NM_EMPTY_GROUP)) + g_key_file_remove_key(kf, group, k, NULL); + else if (!has_key) { + g_debug("NetworkManager: passing through fallback key: %s.%s=%s", group, k, val); + g_key_file_set_comment(kf, group, k, "Netplan: passthrough setting", NULL); + } else if (!!g_strcmp0(val, old_key)) { + g_debug("NetworkManager: fallback override: %s.%s=%s", group, k, val); + g_key_file_set_comment(kf, group, k, "Netplan: passthrough override", NULL); + } + + g_strfreev(group_key); +} + +/** + * Generate NetworkManager configuration in @rootdir/run/NetworkManager/ for a + * particular NetplanNetDefinition and NetplanWifiAccessPoint, as NM requires a separate + * connection file for each SSID. + * @def: The NetplanNetDefinition for which to create a connection + * @rootdir: If not %NULL, generate configuration in this root directory + * (useful for testing). + * @ap: The access point for which to create a connection. Must be %NULL for + * non-wifi types. + */ +static void +write_nm_conf_access_point(NetplanNetDefinition* def, const char* rootdir, const NetplanWifiAccessPoint* ap) +{ + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar* conf_path = NULL; + g_autofree gchar* full_path = NULL; + g_autofree gchar* nd_nm_id = NULL; + const gchar* nm_type = NULL; + gchar* tmp_key = NULL; + mode_t orig_umask; + char uuidstr[37]; + const char *match_interface_name = NULL; + + if (def->type == NETPLAN_DEF_TYPE_WIFI) + g_assert(ap); + else + g_assert(ap == NULL); + + if (def->type == NETPLAN_DEF_TYPE_VLAN && def->sriov_vlan_filter) { + g_debug("%s is defined as a hardware SR-IOV filtered VLAN, postponing creation", def->id); + return; + } + + kf = g_key_file_new(); + if (ap && ap->backend_settings.nm.name) + g_key_file_set_string(kf, "connection", "id", ap->backend_settings.nm.name); + else if (def->backend_settings.nm.name) + g_key_file_set_string(kf, "connection", "id", def->backend_settings.nm.name); + else { + /* Auto-generate a name for the connection profile, if not specified */ + if (ap) + nd_nm_id = g_strdup_printf("netplan-%s-%s", def->id, ap->ssid); + else + nd_nm_id = g_strdup_printf("netplan-%s", def->id); + g_key_file_set_string(kf, "connection", "id", nd_nm_id); + } + + nm_type = type_str(def); + if (nm_type) + g_key_file_set_string(kf, "connection", "type", nm_type); + + if (ap && ap->backend_settings.nm.uuid) + g_key_file_set_string(kf, "connection", "uuid", ap->backend_settings.nm.uuid); + else if (def->backend_settings.nm.uuid) + g_key_file_set_string(kf, "connection", "uuid", def->backend_settings.nm.uuid); + /* VLAN devices refer to us as their parent; if our ID is not a name but we + * have matches, parent= must be the connection UUID, so put it into the + * connection */ + if (def->has_vlans && def->has_match) { + maybe_generate_uuid(def); + uuid_unparse(def->uuid, uuidstr); + g_key_file_set_string(kf, "connection", "uuid", uuidstr); + } + + if (def->activation_mode) { + /* XXX: For now NetworkManager only supports the "manual" activation + * mode */ + if (!!g_strcmp0(def->activation_mode, "manual")) { + g_fprintf(stderr, "ERROR: %s: NetworkManager definitions do not support activation-mode %s\n", def->id, def->activation_mode); + exit(1); + } + /* "manual" */ + g_key_file_set_boolean(kf, "connection", "autoconnect", FALSE); + } + + if (def->type < NETPLAN_DEF_TYPE_VIRTUAL) { + /* physical (existing) devices use matching; driver matching is not + * supported, MAC matching is done below (different keyfile section), + * so only match names here */ + if (def->set_name) + g_key_file_set_string(kf, "connection", "interface-name", def->set_name); + else if (!def->has_match) + g_key_file_set_string(kf, "connection", "interface-name", def->id); + else if (def->match.original_name) { + if (strpbrk(def->match.original_name, "*[]?")) + match_interface_name = def->match.original_name; + else + g_key_file_set_string(kf, "connection", "interface-name", def->match.original_name); + } + /* else matches on something other than the name, do not restrict interface-name */ + } else { + /* virtual (created) devices set a name */ + if (strlen(def->id) > 15) + g_debug("interface-name longer than 15 characters is not supported"); + else + g_key_file_set_string(kf, "connection", "interface-name", def->id); + + if (def->type == NETPLAN_DEF_TYPE_BRIDGE) + write_bridge_params(def, kf); + } + if (def->type == NETPLAN_DEF_TYPE_MODEM) { + const char* modem_type = modem_is_gsm(def) ? "gsm" : "cdma"; + + /* Use NetworkManager's auto configuration feature if no APN, username, or password is specified */ + if (def->modem_params.auto_config || (!def->modem_params.apn && + !def->modem_params.username && !def->modem_params.password)) { + g_key_file_set_boolean(kf, modem_type, "auto-config", TRUE); + } else { + if (def->modem_params.apn) + g_key_file_set_string(kf, modem_type, "apn", def->modem_params.apn); + if (def->modem_params.password) + g_key_file_set_string(kf, modem_type, "password", def->modem_params.password); + if (def->modem_params.username) + g_key_file_set_string(kf, modem_type, "username", def->modem_params.username); + } + + if (def->modem_params.device_id) + g_key_file_set_string(kf, modem_type, "device-id", def->modem_params.device_id); + if (def->mtubytes) + g_key_file_set_uint64(kf, modem_type, "mtu", def->mtubytes); + if (def->modem_params.network_id) + g_key_file_set_string(kf, modem_type, "network-id", def->modem_params.network_id); + if (def->modem_params.number) + g_key_file_set_string(kf, modem_type, "number", def->modem_params.number); + if (def->modem_params.pin) + g_key_file_set_string(kf, modem_type, "pin", def->modem_params.pin); + if (def->modem_params.sim_id) + g_key_file_set_string(kf, modem_type, "sim-id", def->modem_params.sim_id); + if (def->modem_params.sim_operator_id) + g_key_file_set_string(kf, modem_type, "sim-operator-id", def->modem_params.sim_operator_id); + } + if (def->bridge) { + g_key_file_set_string(kf, "connection", "slave-type", "bridge"); + g_key_file_set_string(kf, "connection", "master", def->bridge); + + if (def->bridge_params.path_cost) + g_key_file_set_uint64(kf, "bridge-port", "path-cost", def->bridge_params.path_cost); + if (def->bridge_params.port_priority) + g_key_file_set_uint64(kf, "bridge-port", "priority", def->bridge_params.port_priority); + } + if (def->bond) { + g_key_file_set_string(kf, "connection", "slave-type", "bond"); + g_key_file_set_string(kf, "connection", "master", def->bond); + } + + if (def->ipv6_mtubytes) { + g_fprintf(stderr, "ERROR: %s: NetworkManager definitions do not support ipv6-mtu\n", def->id); + exit(1); + } + + if (def->type < NETPLAN_DEF_TYPE_VIRTUAL) { + if (def->type == NETPLAN_DEF_TYPE_ETHERNET) + g_key_file_set_integer(kf, "ethernet", "wake-on-lan", def->wake_on_lan ? 1 : 0); + + const char* con_type = NULL; + switch (def->type) { + case NETPLAN_DEF_TYPE_WIFI: + con_type = "wifi"; + case NETPLAN_DEF_TYPE_MODEM: + /* Avoid adding an [ethernet] section into the [gsm/cdma] description. */ + break; + default: + con_type = "ethernet"; + } + + if (con_type) { + if (!def->set_name && def->match.mac) + g_key_file_set_string(kf, con_type, "mac-address", def->match.mac); + if (def->set_mac) + g_key_file_set_string(kf, con_type, "cloned-mac-address", def->set_mac); + if (def->mtubytes) + g_key_file_set_uint64(kf, con_type, "mtu", def->mtubytes); + if (def->wowlan && def->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT) + g_key_file_set_uint64(kf, con_type, "wake-on-wlan", def->wowlan); + } + } else { + if (def->set_mac) + g_key_file_set_string(kf, "ethernet", "cloned-mac-address", def->set_mac); + if (def->mtubytes) + g_key_file_set_uint64(kf, "ethernet", "mtu", def->mtubytes); + } + + if (def->type == NETPLAN_DEF_TYPE_VLAN) { + g_assert(def->vlan_id < G_MAXUINT); + g_assert(def->vlan_link != NULL); + g_key_file_set_uint64(kf, "vlan", "id", def->vlan_id); + if (def->vlan_link->has_match) { + /* we need to refer to the parent's UUID as we don't have an + * interface name with match: */ + maybe_generate_uuid(def->vlan_link); + uuid_unparse(def->vlan_link->uuid, uuidstr); + g_key_file_set_string(kf, "vlan", "parent", uuidstr); + } else { + /* if we have an interface name, use that as parent */ + g_key_file_set_string(kf, "vlan", "parent", def->vlan_link->id); + } + } + + if (def->type == NETPLAN_DEF_TYPE_BOND) + write_bond_parameters(def, kf); + + if (def->type == NETPLAN_DEF_TYPE_TUNNEL) { + if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) + write_wireguard_params(def, kf); + else + write_tunnel_params(def, kf); + } + + if (match_interface_name) { + const gchar* list[1] = {match_interface_name}; + g_key_file_set_string_list(kf, "match", "interface-name", list, 1); + } + + if (ap && ap->mode == NETPLAN_WIFI_MODE_AP) + g_key_file_set_string(kf, "ipv4", "method", "shared"); + else if (def->dhcp4) + g_key_file_set_string(kf, "ipv4", "method", "auto"); + else if (def->ip4_addresses) + /* This requires adding at least one address (done below) */ + g_key_file_set_string(kf, "ipv4", "method", "manual"); + else if (def->type == NETPLAN_DEF_TYPE_TUNNEL) + /* sit tunnels will not start in link-local apparently */ + g_key_file_set_string(kf, "ipv4", "method", "disabled"); + else + /* Without any address, this is the only available mode */ + g_key_file_set_string(kf, "ipv4", "method", "link-local"); + + if (def->ip4_addresses) { + for (unsigned i = 0; i < def->ip4_addresses->len; ++i) { + tmp_key = g_strdup_printf("address%i", i+1); + g_key_file_set_string(kf, "ipv4", tmp_key, g_array_index(def->ip4_addresses, char*, i)); + g_free(tmp_key); + } + } + if (def->gateway4) + g_key_file_set_string(kf, "ipv4", "gateway", def->gateway4); + if (def->ip4_nameservers) { + const gchar* list[def->ip4_nameservers->len]; + for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) + list[i] = g_array_index(def->ip4_nameservers, char*, i); + g_key_file_set_string_list(kf, "ipv4", "dns", list, def->ip4_nameservers->len); + } + + /* We can only write search domains and routes if we have an address */ + if (def->ip4_addresses || def->dhcp4) { + write_search_domains(def, "ipv4", kf); + write_routes(def, kf, AF_INET); + } + + if (!def->dhcp4_overrides.use_routes) { + g_key_file_set_boolean(kf, "ipv4", "ignore-auto-routes", TRUE); + g_key_file_set_boolean(kf, "ipv4", "never-default", TRUE); + } + + if (def->dhcp4 && def->dhcp4_overrides.metric != NETPLAN_METRIC_UNSPEC) + g_key_file_set_uint64(kf, "ipv4", "route-metric", def->dhcp4_overrides.metric); + + if (def->dhcp6 || def->ip6_addresses || def->gateway6 || def->ip6_nameservers || def->ip6_addr_gen_mode) { + g_key_file_set_string(kf, "ipv6", "method", def->dhcp6 ? "auto" : "manual"); + + if (def->ip6_addresses) { + for (unsigned i = 0; i < def->ip6_addresses->len; ++i) { + tmp_key = g_strdup_printf("address%i", i+1); + g_key_file_set_string(kf, "ipv6", tmp_key, g_array_index(def->ip6_addresses, char*, i)); + g_free(tmp_key); + } + } + if (def->ip6_addr_gen_token) { + /* Token implies EUI-64, i.e mode=0 */ + g_key_file_set_integer(kf, "ipv6", "addr-gen-mode", 0); + g_key_file_set_string(kf, "ipv6", "token", def->ip6_addr_gen_token); + } else if (def->ip6_addr_gen_mode) + g_key_file_set_string(kf, "ipv6", "addr-gen-mode", addr_gen_mode_str(def->ip6_addr_gen_mode)); + if (def->ip6_privacy) + g_key_file_set_integer(kf, "ipv6", "ip6-privacy", 2); + if (def->gateway6) + g_key_file_set_string(kf, "ipv6", "gateway", def->gateway6); + if (def->ip6_nameservers) { + const gchar* list[def->ip6_nameservers->len]; + for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) + list[i] = g_array_index(def->ip6_nameservers, char*, i); + g_key_file_set_string_list(kf, "ipv6", "dns", list, def->ip6_nameservers->len); + } + /* nm-settings(5) specifies search-domain for both [ipv4] and [ipv6] -- + * We need to specify it here for the IPv6-only case - see LP: #1786726 */ + write_search_domains(def, "ipv6", kf); + + /* We can only write valid routes if there is a DHCPv6 or static IPv6 address */ + write_routes(def, kf, AF_INET6); + + if (!def->dhcp6_overrides.use_routes) { + g_key_file_set_boolean(kf, "ipv6", "ignore-auto-routes", TRUE); + g_key_file_set_boolean(kf, "ipv6", "never-default", TRUE); + } + + if (def->dhcp6_overrides.metric != NETPLAN_METRIC_UNSPEC) + g_key_file_set_uint64(kf, "ipv6", "route-metric", def->dhcp6_overrides.metric); + } + else + g_key_file_set_string(kf, "ipv6", "method", "ignore"); + + if (def->backend_settings.nm.passthrough) { + g_debug("NetworkManager: using keyfile passthrough mode"); + /* Write all key-value pairs from the hashtable into the keyfile, + * potentially overriding existing values, if not fully supported. */ + g_datalist_foreach(&def->backend_settings.nm.passthrough, write_fallback_key_value, kf); + } + + if (ap) { + g_autofree char* escaped_ssid = g_uri_escape_string(ap->ssid, NULL, TRUE); + conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, "-", escaped_ssid, ".nmconnection", NULL); + + g_key_file_set_string(kf, "wifi", "ssid", ap->ssid); + if (ap->mode < NETPLAN_WIFI_MODE_OTHER) + g_key_file_set_string(kf, "wifi", "mode", wifi_mode_str(ap->mode)); + if (ap->bssid) + g_key_file_set_string(kf, "wifi", "bssid", ap->bssid); + if (ap->hidden) + g_key_file_set_boolean(kf, "wifi", "hidden", TRUE); + if (ap->band == NETPLAN_WIFI_BAND_5 || ap->band == NETPLAN_WIFI_BAND_24) { + g_key_file_set_string(kf, "wifi", "band", wifi_band_str(ap->band)); + /* Channel is only unambiguous, if band is set. */ + if (ap->channel) { + /* Validate WiFi channel */ + if (ap->band == NETPLAN_WIFI_BAND_5) + wifi_get_freq5(ap->channel); + else + wifi_get_freq24(ap->channel); + g_key_file_set_uint64(kf, "wifi", "channel", ap->channel); + } + } + if (ap->has_auth) { + write_wifi_auth_parameters(&ap->auth, kf); + } + if (ap->backend_settings.nm.passthrough) { + g_debug("NetworkManager: using AP keyfile passthrough mode"); + /* Write all key-value pairs from the hashtable into the keyfile, + * potentially overriding existing values, if not fully supported. + * AP passthrough values have higher priority than ND passthrough, + * because they are more specific and bound to the current SSID's + * NM connection profile. */ + g_datalist_foreach((GData**)&ap->backend_settings.nm.passthrough, write_fallback_key_value, kf); + } + } else { + conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, ".nmconnection", NULL); + if (def->has_auth) { + write_dot1x_auth_parameters(&def->auth, kf); + } + } + + /* NM connection files might contain secrets, and NM insists on tight permissions */ + full_path = g_strjoin(G_DIR_SEPARATOR_S, rootdir ?: "", conf_path, NULL); + orig_umask = umask(077); + safe_mkdir_p_dir(full_path); + if (!g_key_file_save_to_file(kf, full_path, &error)) { + // LCOV_EXCL_START + g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", full_path, error->message); + exit(1); + // LCOV_EXCL_STO + } + umask(orig_umask); +} + +/** + * Generate NetworkManager configuration in @rootdir/run/NetworkManager/ for a + * particular NetplanNetDefinition. + * @rootdir: If not %NULL, generate configuration in this root directory + * (useful for testing). + */ +void +write_nm_conf(NetplanNetDefinition* def, const char* rootdir) +{ + if (def->backend != NETPLAN_BACKEND_NM) { + g_debug("NetworkManager: definition %s is not for us (backend %i)", def->id, def->backend); + return; + } + + if (def->match.driver && !def->set_name) { + g_fprintf(stderr, "ERROR: %s: NetworkManager definitions do not support matching by driver\n", def->id); + exit(1); + } + + if (def->address_options) { + g_fprintf(stderr, "ERROR: %s: NetworkManager does not support address options\n", def->id); + exit(1); + } + + if (def->type == NETPLAN_DEF_TYPE_WIFI) { + GHashTableIter iter; + gpointer key; + const NetplanWifiAccessPoint* ap; + g_assert(def->access_points); + g_hash_table_iter_init(&iter, def->access_points); + while (g_hash_table_iter_next(&iter, &key, (gpointer) &ap)) + write_nm_conf_access_point(def, rootdir, ap); + } else { + g_assert(def->access_points == NULL); + write_nm_conf_access_point(def, rootdir, NULL); + } +} + +static void +nd_append_non_nm_ids(gpointer data, gpointer str) +{ + const NetplanNetDefinition* nd = data; + + if (nd->backend != NETPLAN_BACKEND_NM) { + if (nd->match.driver) { + /* TODO: NetworkManager supports (non-globbing) "driver:..." matching nowadays */ + /* NM cannot match on drivers, so ignore these via udev rules */ + if (!udev_rules) + udev_rules = g_string_new(NULL); + g_string_append_printf(udev_rules, "ACTION==\"add|change\", SUBSYSTEM==\"net\", ENV{ID_NET_DRIVER}==\"%s\", ENV{NM_UNMANAGED}=\"1\"\n", nd->match.driver); + } else { + g_string_append_netdef_match((GString*) str, nd); + } + } +} + +void +write_nm_conf_finish(const char* rootdir) +{ + GString *s = NULL; + gsize len; + + if (!netdefs || g_hash_table_size(netdefs) == 0) + return; + + /* Set all devices not managed by us to unmanaged, so that NM does not + * auto-connect and interferes */ + s = g_string_new("[keyfile]\n# devices managed by networkd\nunmanaged-devices+="); + len = s->len; + g_list_foreach(netdefs_ordered, nd_append_non_nm_ids, s); + if (s->len > len) + g_string_free_to_file(s, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL); + else + g_string_free(s, TRUE); + + /* write generated udev rules */ + if (udev_rules) + g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL); +} + +/** + * Clean up all generated configurations in @rootdir from previous runs. + */ +void +cleanup_nm_conf(const char* rootdir) +{ + g_autofree char* confpath = g_strjoin(NULL, rootdir ?: "", "/run/NetworkManager/conf.d/netplan.conf", NULL); + g_autofree char* global_manage_path = g_strjoin(NULL, rootdir ?: "", "/run/NetworkManager/conf.d/10-globally-managed-devices.conf", NULL); + unlink(confpath); + unlink(global_manage_path); + unlink_glob(rootdir, "/run/NetworkManager/system-connections/netplan-*"); +} diff --git a/src/nm.h b/src/nm.h new file mode 100644 index 0000000..9f8f9ca --- /dev/null +++ b/src/nm.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 Canonical, Ltd. + * Author: Martin Pitt + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "parse.h" + +void write_nm_conf(NetplanNetDefinition* def, const char* rootdir); +void write_nm_conf_finish(const char* rootdir); +void cleanup_nm_conf(const char* rootdir); diff --git a/src/openvswitch.c b/src/openvswitch.c new file mode 100644 index 0000000..2088fbe --- /dev/null +++ b/src/openvswitch.c @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2020 Canonical, Ltd. + * Author: Łukasz 'sil2100' Zemczak + * Lukas 'slyon' Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include +#include + +#include "openvswitch.h" +#include "networkd.h" +#include "parse.h" +#include "util.h" + +static void +write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, gboolean physical, gboolean cleanup, const char* dependency) +{ + g_autofree gchar* id_escaped = NULL; + g_autofree char* link = g_strjoin(NULL, rootdir ?: "", "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-", id, ".service", NULL); + g_autofree char* path = g_strjoin(NULL, "/run/systemd/system/netplan-ovs-", id, ".service", NULL); + + GString* s = g_string_new("[Unit]\n"); + g_string_append_printf(s, "Description=OpenVSwitch configuration for %s\n", id); + g_string_append(s, "DefaultDependencies=no\n"); + /* run any ovs-netplan unit only after openvswitch-switch.service is ready */ + g_string_append_printf(s, "Wants=ovsdb-server.service\n"); + g_string_append_printf(s, "After=ovsdb-server.service\n"); + if (physical) { + id_escaped = systemd_escape((char*) id); + g_string_append_printf(s, "Requires=sys-subsystem-net-devices-%s.device\n", id_escaped); + g_string_append_printf(s, "After=sys-subsystem-net-devices-%s.device\n", id_escaped); + } + if (!cleanup) { + g_string_append_printf(s, "After=netplan-ovs-cleanup.service\n"); + } else { + /* The netplan-ovs-cleanup unit shall not run on systems where openvswitch is not installed. */ + g_string_append(s, "ConditionFileIsExecutable=" OPENVSWITCH_OVS_VSCTL "\n"); + } + g_string_append(s, "Before=network.target\nWants=network.target\n"); + if (dependency) { + g_string_append_printf(s, "Requires=netplan-ovs-%s.service\n", dependency); + g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency); + } + + g_string_append(s, "\n[Service]\nType=oneshot\n"); + g_string_append(s, cmds->str); + + g_string_free_to_file(s, rootdir, path, NULL); + + safe_mkdir_p_dir(link); + if (symlink(path, link) < 0 && errno != EEXIST) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to create enablement symlink: %m\n"); + exit(1); + // LCOV_EXCL_STOP + } +} + +#define append_systemd_cmd(s, command, ...) \ +{ \ + g_string_append(s, "ExecStart="); \ + g_string_append_printf(s, command, __VA_ARGS__); \ + g_string_append(s, "\n"); \ +} + +static char* +netplan_type_to_table_name(const NetplanDefType type) +{ + switch (type) { + case NETPLAN_DEF_TYPE_BRIDGE: + return "Bridge"; + case NETPLAN_DEF_TYPE_BOND: + case NETPLAN_DEF_TYPE_PORT: + return "Port"; + default: /* For regular interfaces and others */ + return "Interface"; + } +} + +static gboolean +netplan_type_is_physical(const NetplanDefType type) +{ + switch (type) { + case NETPLAN_DEF_TYPE_ETHERNET: + // case NETPLAN_DEF_TYPE_WIFI: + // case NETPLAN_DEF_TYPE_MODEM: + return TRUE; + default: + return FALSE; + } +} + +static void +write_ovs_tag_setting(const gchar* id, const char* type, const char* col, const char* key, const char* value, GString* cmds) +{ + g_assert(col); + g_assert(value); + g_autofree char *clean_value = g_strdup(value); + /* Replace " " -> "," if value contains spaces */ + if (strchr(value, ' ')) { + char **split = g_strsplit(value, " ", -1); + g_free(clean_value); + clean_value = g_strjoinv(",", split); + g_strfreev(split); + } + + GString* s = g_string_new("external-ids:netplan/"); + g_string_append_printf(s, "%s", col); + if (key) + g_string_append_printf(s, "/%s", key); + g_string_append_printf(s, "=%s", clean_value); + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s %s", type, id, s->str); + g_string_free(s, TRUE); +} + +static void +write_ovs_additional_data(GHashTable *data, const char* type, const gchar* id, GString* cmds, const char* setting) +{ + GHashTableIter iter; + gchar* key; + gchar* value; + + g_hash_table_iter_init(&iter, data); + while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &value)) { + /* XXX: we need to check what happens when an invalid key=value pair + gets supplied here. We might want to handle this somehow. */ + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s %s:%s=%s", + type, id, setting, key, value); + write_ovs_tag_setting(id, type, setting, key, value, cmds); + } +} + +static void +setup_patch_port(GString* s, const NetplanNetDefinition* def) +{ + /* Execute the setup commands to create an OVS patch port atomically within + * the same command where this virtual interface is created. Either as a + * Port+Interface of an OVS bridge or as a Interface of an OVS bond. This + * avoids delays in the PatchPort creation and thus potential races. */ + g_assert(def->type == NETPLAN_DEF_TYPE_PORT); + g_string_append_printf(s, " -- set Interface %s type=patch options:peer=%s", + def->id, def->peer); +} + +static char* +write_ovs_bond_interfaces(const NetplanNetDefinition* def, GString* cmds) +{ + NetplanNetDefinition* tmp_nd; + GHashTableIter iter; + gchar* key; + guint i = 0; + GString* s = NULL; + GString* patch_ports = g_string_new(""); + + if (!def->bridge) { + g_fprintf(stderr, "Bond %s needs to be a slave of an OpenVSwitch bridge\n", def->id); + exit(1); + } + + s = g_string_new(OPENVSWITCH_OVS_VSCTL " --may-exist add-bond"); + g_string_append_printf(s, " %s %s", def->bridge, def->id); + + g_hash_table_iter_init(&iter, netdefs); + while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &tmp_nd)) { + if (!g_strcmp0(def->id, tmp_nd->bond)) { + /* Append and count bond interfaces */ + g_string_append_printf(s, " %s", tmp_nd->id); + i++; + if (tmp_nd->type == NETPLAN_DEF_TYPE_PORT) + setup_patch_port(patch_ports, tmp_nd); + } + } + if (i < 2) { + g_fprintf(stderr, "Bond %s needs to have at least 2 slave interfaces\n", def->id); + exit(1); + } + + g_string_append(s, patch_ports->str); + g_string_free(patch_ports, TRUE); + append_systemd_cmd(cmds, s->str, def->bridge, def->id); + g_string_free(s, TRUE); + return def->bridge; +} + +static void +write_ovs_tag_netplan(const gchar* id, const char* type, GString* cmds) +{ + /* Mark this bridge/port/interface as created by netplan */ + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set %s %s external-ids:netplan=true", + type, id); +} + +static void +write_ovs_bond_mode(const NetplanNetDefinition* def, GString* cmds) +{ + char* value = NULL; + /* OVS supports only "active-backup", "balance-tcp" and "balance-slb": + * http://www.openvswitch.org/support/dist-docs/ovs-vswitchd.conf.db.5.txt */ + if (!strcmp(def->bond_params.mode, "active-backup") || + !strcmp(def->bond_params.mode, "balance-tcp") || + !strcmp(def->bond_params.mode, "balance-slb")) { + value = def->bond_params.mode; + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Port %s bond_mode=%s", def->id, value); + write_ovs_tag_setting(def->id, "Port", "bond_mode", NULL, value, cmds); + } else { + g_fprintf(stderr, "%s: bond mode '%s' not supported by openvswitch\n", + def->id, def->bond_params.mode); + exit(1); + } +} + +static void +write_ovs_bridge_interfaces(const NetplanNetDefinition* def, GString* cmds) +{ + NetplanNetDefinition* tmp_nd; + GHashTableIter iter; + gchar* key; + + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-br %s", def->id); + + g_hash_table_iter_init(&iter, netdefs); + while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &tmp_nd)) { + /* OVS bonds will connect to their OVS bridge and create the interface/port themselves */ + if ((tmp_nd->type != NETPLAN_DEF_TYPE_BOND || tmp_nd->backend != NETPLAN_BACKEND_OVS) + && !g_strcmp0(def->id, tmp_nd->bridge)) { + GString * patch_ports = g_string_new(""); + if (tmp_nd->type == NETPLAN_DEF_TYPE_PORT) + setup_patch_port(patch_ports, tmp_nd); + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-port %s %s%s", + def->id, tmp_nd->id, patch_ports->str); + g_string_free(patch_ports, TRUE); + } + } +} + +static void +write_ovs_protocols(const NetplanOVSSettings* ovs_settings, const gchar* bridge, GString* cmds) +{ + g_assert(bridge); + GString* s = g_string_new(g_array_index(ovs_settings->protocols, char*, 0)); + + for (unsigned i = 1; i < ovs_settings->protocols->len; ++i) + g_string_append_printf(s, ",%s", g_array_index(ovs_settings->protocols, char*, i)); + + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s protocols=%s", bridge, s->str); + write_ovs_tag_setting(bridge, "Bridge", "protocols", NULL, s->str, cmds); + g_string_free(s, TRUE); +} + +static gboolean +check_ovs_ssl(gchar* target) +{ + /* Check if target needs ssl */ + if (g_str_has_prefix(target, "ssl:") || g_str_has_prefix(target, "pssl:")) { + /* Check if SSL is configured in ovs_settings_global.ssl */ + if (!ovs_settings_global.ssl.ca_certificate || !ovs_settings_global.ssl.client_certificate || + !ovs_settings_global.ssl.client_key) { + g_fprintf(stderr, "ERROR: openvswitch bridge controller target '%s' needs SSL configuration, but global 'openvswitch.ssl' settings are not set\n", target); + exit(1); + } + return TRUE; + } + return FALSE; +} + +static void +write_ovs_bridge_controller_targets(const NetplanOVSController* controller, const gchar* bridge, GString* cmds) +{ + gchar* target = g_array_index(controller->addresses, char*, 0); + gboolean needs_ssl = check_ovs_ssl(target); + GString* s = g_string_new(target); + + for (unsigned i = 1; i < controller->addresses->len; ++i) { + target = g_array_index(controller->addresses, char*, i); + if (!needs_ssl) + needs_ssl = check_ovs_ssl(target); + g_string_append_printf(s, " %s", target); + } + + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-controller %s %s", bridge, s->str); + write_ovs_tag_setting(bridge, "Bridge", "global", "set-controller", s->str, cmds); + g_string_free(s, TRUE); +} + +/** + * Generate the OpenVSwitch systemd units for configuration of the selected netdef + * @rootdir: If not %NULL, generate configuration in this root directory + * (useful for testing). + */ +void +write_ovs_conf(const NetplanNetDefinition* def, const char* rootdir) +{ + GString* cmds = g_string_new(NULL); + gchar* dependency = NULL; + const char* type = netplan_type_to_table_name(def->type); + g_autofree char* base_config_path = NULL; + char* value = NULL; + + /* TODO: maybe dynamically query the ovs-vsctl tool path? */ + + /* For OVS specific settings, we expect the backend to be set to OVS. + * The OVS backend is implicitly set, if an interface contains an empty "openvswitch: {}" + * key, or an "openvswitch:" key, containing more than "external-ids" and/or "other-config". */ + if (def->backend == NETPLAN_BACKEND_OVS) { + switch (def->type) { + case NETPLAN_DEF_TYPE_BOND: + dependency = write_ovs_bond_interfaces(def, cmds); + write_ovs_tag_netplan(def->id, type, cmds); + /* Set LACP mode, default to "off" */ + value = def->ovs_settings.lacp? def->ovs_settings.lacp : "off"; + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Port %s lacp=%s", def->id, value); + write_ovs_tag_setting(def->id, type, "lacp", NULL, value, cmds); + if (def->bond_params.mode) { + write_ovs_bond_mode(def, cmds); + } + break; + + case NETPLAN_DEF_TYPE_BRIDGE: + write_ovs_bridge_interfaces(def, cmds); + write_ovs_tag_netplan(def->id, type, cmds); + /* Set fail-mode, default to "standalone" */ + value = def->ovs_settings.fail_mode? def->ovs_settings.fail_mode : "standalone"; + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-fail-mode %s %s", def->id, value); + write_ovs_tag_setting(def->id, type, "global", "set-fail-mode", value, cmds); + /* Enable/disable mcast-snooping */ + value = def->ovs_settings.mcast_snooping? "true" : "false"; + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s mcast_snooping_enable=%s", def->id, value); + write_ovs_tag_setting(def->id, type, "mcast_snooping_enable", NULL, value, cmds); + /* Enable/disable rstp */ + value = def->ovs_settings.rstp? "true" : "false"; + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Bridge %s rstp_enable=%s", def->id, value); + write_ovs_tag_setting(def->id, type, "rstp_enable", NULL, value, cmds); + /* Set protocols */ + if (def->ovs_settings.protocols && def->ovs_settings.protocols->len > 0) { + write_ovs_protocols(&(def->ovs_settings), def->id, cmds); + } else if (ovs_settings_global.protocols && ovs_settings_global.protocols->len > 0) { + write_ovs_protocols(&(ovs_settings_global), def->id, cmds); + } + /* Set controller target addresses */ + if (def->ovs_settings.controller.addresses && def->ovs_settings.controller.addresses->len > 0) { + write_ovs_bridge_controller_targets(&(def->ovs_settings.controller), def->id, cmds); + /* Set controller connection mode, only applicable if at least one controller target address was set */ + if (def->ovs_settings.controller.connection_mode) { + value = def->ovs_settings.controller.connection_mode; + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set Controller %s connection-mode=%s", def->id, value); + write_ovs_tag_setting(def->id, "Controller", "connection-mode", NULL, value, cmds); + } + } + break; + + case NETPLAN_DEF_TYPE_PORT: + g_assert(def->peer); + dependency = def->bridge?: def->bond; + if (!dependency) { + g_fprintf(stderr, "%s: OpenVSwitch patch port needs to be assigned to a bridge/bond\n", def->id); + exit(1); + } + /* There is no OVS Port which we could tag netplan=true if this + * patch port is assigned as an OVS bond interface. Tag the + * Interface instead, to clean it up from a bond. */ + if (def->bond) + write_ovs_tag_netplan(def->id, "Interface", cmds); + else + write_ovs_tag_netplan(def->id, type, cmds); + break; + + case NETPLAN_DEF_TYPE_VLAN: + g_assert(def->vlan_link); + dependency = def->vlan_link->id; + /* Create a fake VLAN bridge */ + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " --may-exist add-br %s %s %i", def->id, def->vlan_link->id, def->vlan_id) + write_ovs_tag_netplan(def->id, type, cmds); + break; + + default: + g_fprintf(stderr, "%s: This device type is not supported with the OpenVSwitch backend\n", def->id); + exit(1); + break; + } + + /* Try writing out a base config */ + base_config_path = g_strjoin(NULL, "run/systemd/network/10-netplan-", def->id, NULL); + write_network_file(def, rootdir, base_config_path); + } else { + /* Other interfaces must be part of an OVS bridge or bond to carry additional data */ + if ( (def->ovs_settings.external_ids && g_hash_table_size(def->ovs_settings.external_ids) > 0) + || (def->ovs_settings.other_config && g_hash_table_size(def->ovs_settings.other_config) > 0)) { + dependency = def->bridge?: def->bond; + if (!dependency) { + g_fprintf(stderr, "%s: Interface needs to be assigned to an OVS bridge/bond to carry external-ids/other-config\n", def->id); + exit(1); + } + } else { + g_debug("openvswitch: definition %s is not for us (backend %i)", def->id, def->backend); + return; + } + } + + /* Set "external-ids" and "other-config" after NETPLAN_BACKEND_OVS interfaces, as bonds, + * bridges, etc. might just be created before.*/ + + /* Common OVS settings can be specified even for non-OVS interfaces */ + if (def->ovs_settings.external_ids && g_hash_table_size(def->ovs_settings.external_ids) > 0) { + write_ovs_additional_data(def->ovs_settings.external_ids, type, + def->id, cmds, "external-ids"); + } + + if (def->ovs_settings.other_config && g_hash_table_size(def->ovs_settings.other_config) > 0) { + write_ovs_additional_data(def->ovs_settings.other_config, type, + def->id, cmds, "other-config"); + } + + /* If we need to configure anything for this netdef, write the required systemd unit */ + if (cmds->len > 0) + write_ovs_systemd_unit(def->id, cmds, rootdir, netplan_type_is_physical(def->type), FALSE, dependency); + g_string_free(cmds, TRUE); +} + +/** + * Finalize the OpenVSwitch configuration (global config) + */ +void +write_ovs_conf_finish(const char* rootdir) +{ + GString* cmds = g_string_new(NULL); + + /* Global external-ids and other-config settings */ + if (ovs_settings_global.external_ids && g_hash_table_size(ovs_settings_global.external_ids) > 0) { + write_ovs_additional_data(ovs_settings_global.external_ids, "open_vswitch", + ".", cmds, "external-ids"); + } + + if (ovs_settings_global.other_config && g_hash_table_size(ovs_settings_global.other_config) > 0) { + write_ovs_additional_data(ovs_settings_global.other_config, "open_vswitch", + ".", cmds, "other-config"); + } + + if (ovs_settings_global.ssl.client_key && ovs_settings_global.ssl.client_certificate && + ovs_settings_global.ssl.ca_certificate) { + GString* value = g_string_new(NULL); + g_string_printf(value, "%s %s %s", + ovs_settings_global.ssl.client_key, + ovs_settings_global.ssl.client_certificate, + ovs_settings_global.ssl.ca_certificate); + append_systemd_cmd(cmds, OPENVSWITCH_OVS_VSCTL " set-ssl %s", value->str); + write_ovs_tag_setting(".", "open_vswitch", "global", "set-ssl", value->str, cmds); + g_string_free(value, TRUE); + } + + if (cmds->len > 0) + write_ovs_systemd_unit("global", cmds, rootdir, FALSE, FALSE, NULL); + g_string_free(cmds, TRUE); + + /* Clear all netplan=true tagged ports/bonds and bridges, via 'netplan apply --only-ovs-cleanup' */ + cmds = g_string_new(NULL); + append_systemd_cmd(cmds, SBINDIR "/netplan apply %s", "--only-ovs-cleanup"); + write_ovs_systemd_unit("cleanup", cmds, rootdir, FALSE, TRUE, NULL); + g_string_free(cmds, TRUE); +} + +/** + * Clean up all generated configurations in @rootdir from previous runs. + */ +void +cleanup_ovs_conf(const char* rootdir) +{ + unlink_glob(rootdir, "/run/systemd/system/systemd-networkd.service.wants/netplan-ovs-*.service"); + unlink_glob(rootdir, "/run/systemd/system/netplan-ovs-*.service"); +} diff --git a/src/openvswitch.h b/src/openvswitch.h new file mode 100644 index 0000000..69bd6ee --- /dev/null +++ b/src/openvswitch.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2020 Canonical, Ltd. + * Author: Łukasz 'sil2100' Zemczak + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "parse.h" + +void write_ovs_conf(const NetplanNetDefinition* def, const char* rootdir); +void write_ovs_conf_finish(const char* rootdir); +void cleanup_ovs_conf(const char* rootdir); diff --git a/src/parse-nm.c b/src/parse-nm.c new file mode 100644 index 0000000..9b09e34 --- /dev/null +++ b/src/parse-nm.c @@ -0,0 +1,737 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include "netplan.h" +#include "parse-nm.h" +#include "parse.h" +#include "util.h" + +/** + * NetworkManager writes the alias for '802-3-ethernet' (ethernet), + * '802-11-wireless' (wifi) and '802-11-wireless-security' (wifi-security) + * by default, so we only need to check for those. See: + * https://bugzilla.gnome.org/show_bug.cgi?id=696940 + * https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/c36200a225aefb2a3919618e75682646899b82c0 + */ +static const NetplanDefType +type_from_str(const char* type_str) +{ + if (!g_strcmp0(type_str, "ethernet") || !g_strcmp0(type_str, "802-3-ethernet")) + return NETPLAN_DEF_TYPE_ETHERNET; + else if (!g_strcmp0(type_str, "wifi") || !g_strcmp0(type_str, "802-11-wireless")) + return NETPLAN_DEF_TYPE_WIFI; + else if (!g_strcmp0(type_str, "gsm") || !g_strcmp0(type_str, "cdma")) + return NETPLAN_DEF_TYPE_MODEM; + else if (!g_strcmp0(type_str, "bridge")) + return NETPLAN_DEF_TYPE_BRIDGE; + else if (!g_strcmp0(type_str, "bond")) + return NETPLAN_DEF_TYPE_BOND; + else if (!g_strcmp0(type_str, "vlan")) + return NETPLAN_DEF_TYPE_VLAN; + else if (!g_strcmp0(type_str, "ip-tunnel") || !g_strcmp0(type_str, "wireguard")) + return NETPLAN_DEF_TYPE_TUNNEL; + /* Unsupported type, needs to be specified via passthrough */ + return NETPLAN_DEF_TYPE_NM; +} + +static const NetplanWifiMode +ap_type_from_str(const char* type_str) +{ + if (!g_strcmp0(type_str, "infrastructure")) + return NETPLAN_WIFI_MODE_INFRASTRUCTURE; + else if (!g_strcmp0(type_str, "ap")) + return NETPLAN_WIFI_MODE_AP; + else if (!g_strcmp0(type_str, "adhoc")) + return NETPLAN_WIFI_MODE_ADHOC; + /* Unsupported mode, like "mesh" */ + return NETPLAN_WIFI_MODE_OTHER; +} + +static void +_kf_clear_key(GKeyFile* kf, const gchar* group, const gchar* key) +{ + gsize len = 1; + g_key_file_remove_key(kf, group, key, NULL); + g_strfreev(g_key_file_get_keys(kf, group, &len, NULL)); + /* clear group if this was the last key */ + if (len == 0) + g_key_file_remove_group(kf, group, NULL); +} + +static gboolean +kf_matches(GKeyFile* kf, const gchar* group, const gchar* key, const gchar* match) +{ + g_autofree gchar *kf_value = g_key_file_get_string(kf, group, key, NULL); + return g_strcmp0(kf_value, match) == 0; +} + +static void +set_true_on_match(GKeyFile* kf, const gchar* group, const gchar* key, const gchar* match, const void* dataptr) +{ + g_assert(dataptr); + if (kf_matches(kf, group, key, match)) { + *((gboolean*) dataptr) = TRUE; + _kf_clear_key(kf, group, key); + } +} + +static void +handle_generic_bool(GKeyFile* kf, const gchar* group, const gchar* key, gboolean* dataptr) +{ + g_assert(dataptr); + *dataptr = g_key_file_get_boolean(kf, group, key, NULL); + _kf_clear_key(kf, group, key); +} + +static void +handle_generic_str(GKeyFile* kf, const gchar* group, const gchar* key, char** dataptr) +{ + g_assert(dataptr); + g_assert(!*dataptr); + *dataptr = g_key_file_get_string(kf, group, key, NULL); + if (*dataptr) + _kf_clear_key(kf, group, key); +} + +static void +handle_generic_uint(GKeyFile* kf, const gchar* group, const gchar* key, guint* dataptr, guint default_value) +{ + g_assert(dataptr); + if (g_key_file_has_key(kf, group, key, NULL)) { + guint data = g_key_file_get_uint64(kf, group, key, NULL); + if (data != default_value) + *dataptr = data; + _kf_clear_key(kf, group, key); + } +} + +static void +handle_common(GKeyFile* kf, NetplanNetDefinition* nd, const gchar* group) { + handle_generic_str(kf, group, "cloned-mac-address", &nd->set_mac); + handle_generic_uint(kf, group, "mtu", &nd->mtubytes, NETPLAN_MTU_UNSPEC); + handle_generic_str(kf, group, "mac-address", &nd->match.mac); + if (nd->match.mac) + nd->has_match = TRUE; +} + +static void +handle_bridge_uint(GKeyFile* kf, const gchar* key, NetplanNetDefinition* nd, char** dataptr) { + if (g_key_file_get_uint64(kf, "bridge", key, NULL)) { + nd->custom_bridging = TRUE; + *dataptr = g_strdup_printf("%lu", g_key_file_get_uint64(kf, "bridge", key, NULL)); + _kf_clear_key(kf, "bridge", key); + } +} + +static void +parse_addresses(GKeyFile* kf, const gchar* group, GArray** ip_arr) +{ + g_assert(ip_arr); + if (kf_matches(kf, group, "method", "manual")) { + gboolean unhandled_data = FALSE; + gchar *key = NULL; + gchar *kf_value = NULL; + gchar **split = NULL; + for (unsigned i = 1;; ++i) { + key = g_strdup_printf("address%u", i); + kf_value = g_key_file_get_string(kf, group, key, NULL); + if (!kf_value) { + g_free(key); + break; + } + if (!*ip_arr) + *ip_arr = g_array_new(FALSE, FALSE, sizeof(char*)); + split = g_strsplit(kf_value, ",", 2); + g_free(kf_value); + /* Append "address/prefix" */ + if (split[0]) { + /* no need to free 's', this will stay in the netdef */ + gchar* s = g_strdup(split[0]); + g_array_append_val(*ip_arr, s); + } + if (!split[1]) + _kf_clear_key(kf, group, key); + else + /* XXX: how to handle additional values (like "gateway") in split[n]? */ + unhandled_data = TRUE; + g_strfreev(split); + g_free(key); + } + /* clear keyfile once all data was handled */ + if (!unhandled_data) + _kf_clear_key(kf, group, "method"); + } +} + +static void +parse_routes(GKeyFile* kf, const gchar* group, GArray** routes_arr) +{ + g_assert(routes_arr); + NetplanIPRoute *route = NULL; + gchar *key = NULL; + gchar *kf_value = NULL; + gchar *options_key = NULL; + gchar *options_kf_value = NULL; + gchar **split = NULL; + for (unsigned i = 1;; ++i) { + gboolean unhandled_data = FALSE; + key = g_strdup_printf("route%u", i); + kf_value = g_key_file_get_string(kf, group, key, NULL); + options_key = g_strdup_printf("route%u_options", i); + options_kf_value = g_key_file_get_string(kf, group, options_key, NULL); + if (!options_kf_value) + g_free(options_key); + if (!kf_value) { + g_free(key); + break; + } + if (!*routes_arr) + *routes_arr = g_array_new(FALSE, TRUE, sizeof(NetplanIPRoute*)); + route = g_new0(NetplanIPRoute, 1); + route->type = g_strdup("unicast"); + route->scope = g_strdup("global"); + route->family = G_MAXUINT; /* 0 is a valid family ID */ + route->metric = NETPLAN_METRIC_UNSPEC; /* 0 is a valid metric */ + g_debug("%s: adding new route (kf)", key); + + if (g_strcmp0(group, "ipv4") == 0) + route->family = AF_INET; + else if (g_strcmp0(group, "ipv6") == 0) + route->family = AF_INET6; + + split = g_strsplit(kf_value, ",", 3); + /* Append "to" (address/prefix) */ + if (split[0]) + route->to = g_strdup(split[0]); //no need to free, will stay in netdef + /* Append gateway/via IP */ + if (split[0] && split[1]) + route->via = g_strdup(split[1]); //no need to free, will stay in netdef + /* Append metric */ + if (split[0] && split[1] && split[2] && strtoul(split[2], NULL, 10) != NETPLAN_METRIC_UNSPEC) + route->metric = strtoul(split[2], NULL, 10); + g_strfreev(split); + + /* Parse route options */ + if (options_kf_value) { + g_debug("%s: adding new route_options (kf)", options_key); + split = g_strsplit(options_kf_value, ",", -1); + for (unsigned i = 0; split[i]; ++i) { + g_debug("processing route_option: %s", split[i]); + gchar **kv = g_strsplit(split[i], "=", 2); + if (g_strcmp0(kv[0], "onlink") == 0) + route->onlink = (g_strcmp0(kv[1], "true") == 0); + else if (g_strcmp0(kv[0], "initrwnd") == 0) + route->advertised_receive_window = strtoul(kv[1], NULL, 10); + else if (g_strcmp0(kv[0], "initcwnd") == 0) + route->congestion_window = strtoul(kv[1], NULL, 10); + else if (g_strcmp0(kv[0], "mtu") == 0) + route->mtubytes = strtoul(kv[1], NULL, 10); + else if (g_strcmp0(kv[0], "table") == 0) + route->table = strtoul(kv[1], NULL, 10); + else if (g_strcmp0(kv[0], "src") == 0) + route->from = g_strdup(kv[1]); //no need to free, will stay in netdef + else + unhandled_data = TRUE; + g_strfreev(kv); + } + g_strfreev(split); + + if (!unhandled_data) + _kf_clear_key(kf, group, options_key); + g_free(options_key); + g_free(options_kf_value); + } + + /* Add route to array, clear keyfile */ + g_array_append_val(*routes_arr, route); + if (!unhandled_data) + _kf_clear_key(kf, group, key); + g_free(key); + g_free(kf_value); + } +} + +static void +parse_dhcp_overrides(GKeyFile* kf, const gchar* group, NetplanDHCPOverrides* dataptr) +{ + g_assert(dataptr); + if ( g_key_file_get_boolean(kf, group, "ignore-auto-routes", NULL) + && g_key_file_get_boolean(kf, group, "never-default", NULL)) { + (*dataptr).use_routes = FALSE; + _kf_clear_key(kf, group, "ignore-auto-routes"); + _kf_clear_key(kf, group, "never-default"); + } + handle_generic_uint(kf, group, "route-metric", &(*dataptr).metric, NETPLAN_METRIC_UNSPEC); +} + +static void +parse_search_domains(GKeyFile* kf, const gchar* group, GArray** domains_arr) +{ + g_assert(domains_arr); + gsize len = 0; + gchar **split = g_key_file_get_string_list(kf, group, "dns-search", &len, NULL); + if (split) { + if (len == 0) { + _kf_clear_key(kf, group, "dns-search"); + return; + } + if (!*domains_arr) + *domains_arr = g_array_new(FALSE, FALSE, sizeof(char*)); + for(unsigned i = 0; split[i]; ++i) { + char* s = g_strdup(split[i]); //no need to free, will stay in netdef + g_array_append_val(*domains_arr, s); + } + _kf_clear_key(kf, group, "dns-search"); + g_strfreev(split); + } +} + +static void +parse_nameservers(GKeyFile* kf, const gchar* group, GArray** nameserver_arr) +{ + g_assert(nameserver_arr); + gchar **split = g_key_file_get_string_list(kf, group, "dns", NULL, NULL); + if (split) { + if (!*nameserver_arr) + *nameserver_arr = g_array_new(FALSE, FALSE, sizeof(char*)); + for(unsigned i = 0; split[i]; ++i) { + if (strlen(split[i]) > 0) { + gchar* s = g_strdup(split[i]); //no need to free, will stay in netdef + g_array_append_val(*nameserver_arr, s); + } + } + _kf_clear_key(kf, group, "dns"); + g_strfreev(split); + } +} + +static void +parse_dot1x_auth(GKeyFile* kf, NetplanAuthenticationSettings* auth) +{ + g_assert(auth); + g_autofree gchar* method = g_key_file_get_string(kf, "802-1x", "eap", NULL); + + if (method && g_strcmp0(method, "tls") == 0) { + auth->eap_method = NETPLAN_AUTH_EAP_TLS; + _kf_clear_key(kf, "802-1x", "eap"); + } else if (method && g_strcmp0(method, "peap") == 0) { + auth->eap_method = NETPLAN_AUTH_EAP_PEAP; + _kf_clear_key(kf, "802-1x", "eap"); + } else if (method && g_strcmp0(method, "ttls") == 0) { + auth->eap_method = NETPLAN_AUTH_EAP_TTLS; + _kf_clear_key(kf, "802-1x", "eap"); + } + + handle_generic_str(kf, "802-1x", "identity", &auth->identity); + handle_generic_str(kf, "802-1x", "anonymous-identity", &auth->anonymous_identity); + if (!auth->password) + handle_generic_str(kf, "802-1x", "password", &auth->password); + handle_generic_str(kf, "802-1x", "ca-cert", &auth->ca_certificate); + handle_generic_str(kf, "802-1x", "client-cert", &auth->client_certificate); + handle_generic_str(kf, "802-1x", "private-key", &auth->client_key); + handle_generic_str(kf, "802-1x", "private-key-password", &auth->client_key_password); + handle_generic_str(kf, "802-1x", "phase2-auth", &auth->phase2_auth); +} + +static void +parse_bond_arp_ip_targets(GKeyFile* kf, GArray **targets_arr) +{ + g_assert(targets_arr); + g_autofree gchar *v = g_key_file_get_string(kf, "bond", "arp_ip_target", NULL); + if (v) { + gchar** split = g_strsplit(v, ",", -1); + for (unsigned i = 0; split[i]; ++i) { + if (!*targets_arr) + *targets_arr = g_array_new(FALSE, FALSE, sizeof(char *)); + gchar *s = g_strdup(split[i]); + g_array_append_val(*targets_arr, s); + } + _kf_clear_key(kf, "bond", "arp_ip_target"); + g_strfreev(split); + } +} + +/* Read the key-value pairs from the keyfile and pass them through to a map */ +static void +read_passthrough(GKeyFile* kf, GData** list) +{ + gchar **groups = NULL; + gchar **keys = NULL; + gchar *group_key = NULL; + gchar *value = NULL; + gsize klen = 0; + gsize glen = 0; + + if (!*list) + g_datalist_init(list); + groups = g_key_file_get_groups(kf, &glen); + if (groups) { + for (unsigned i = 0; i < glen; ++i) { + klen = 0; + keys = g_key_file_get_keys(kf, groups[i], &klen, NULL); + if (klen == 0) { + /* empty group */ + g_datalist_set_data_full(list, g_strconcat(groups[i], ".", NETPLAN_NM_EMPTY_GROUP, NULL), g_strdup(""), g_free); + continue; + } + for (unsigned j = 0; j < klen; ++j) { + value = g_key_file_get_string(kf, groups[i], keys[j], NULL); + if (!value) { + // LCOV_EXCL_START + g_warning("netplan: Keyfile: cannot read value of %s.%s", groups[i], keys[j]); + continue; + // LCOV_EXCL_STOP + } + group_key = g_strconcat(groups[i], ".", keys[j], NULL); + g_datalist_set_data_full(list, group_key, value, g_free); + /* no need to free group_key and value: they stay in the list */ + } + g_strfreev(keys); + } + g_strfreev(groups); + } +} + +/** + * Parse keyfile into a NetplanNetDefinition struct + * @filename: full path to the NetworkManager keyfile + */ +gboolean +netplan_parse_keyfile(const char* filename, GError** error) +{ + g_autofree gchar *nd_id = NULL; + g_autofree gchar *uuid = NULL; + g_autofree gchar *type = NULL; + g_autofree gchar* wifi_mode = NULL; + g_autofree gchar* ssid = NULL; + g_autofree gchar* netdef_id = NULL; + gchar *tmp_str = NULL; + NetplanNetDefinition* nd = NULL; + NetplanWifiAccessPoint* ap = NULL; + g_autoptr(GKeyFile) kf = g_key_file_new(); + NetplanDefType nd_type = NETPLAN_DEF_TYPE_NONE; + if (!g_key_file_load_from_file(kf, filename, G_KEY_FILE_NONE, error)) { + g_warning("netplan: cannot load keyfile"); + return FALSE; + } + + ssid = g_key_file_get_string(kf, "wifi", "ssid", NULL); + if (!ssid) + ssid = g_key_file_get_string(kf, "802-11-wireless", "ssid", NULL); + + netdef_id = netplan_get_id_from_nm_filename(filename, ssid); + uuid = g_key_file_get_string(kf, "connection", "uuid", NULL); + if (!uuid) { + g_warning("netplan: Keyfile: cannot find connection.uuid"); + return FALSE; + } + + type = g_key_file_get_string(kf, "connection", "type", NULL); + if (!type) { + g_warning("netplan: Keyfile: cannot find connection.type"); + return FALSE; + } + nd_type = type_from_str(type); + + tmp_str = g_key_file_get_string(kf, "connection", "interface-name", NULL); + /* Use previously existing netdef IDs, if available, to override connections + * Else: generate a "NM-" ID */ + if (netdef_id) { + nd_id = g_strdup(netdef_id); + if (g_strcmp0(netdef_id, tmp_str) == 0) + _kf_clear_key(kf, "connection", "interface-name"); + } else if (tmp_str && nd_type >= NETPLAN_DEF_TYPE_VIRTUAL && nd_type < NETPLAN_DEF_TYPE_NM) { + /* netdef ID equals "interface-name" for virtual devices (bridge/bond/...) */ + nd_id = g_strdup(tmp_str); + _kf_clear_key(kf, "connection", "interface-name"); + } else + nd_id = g_strconcat("NM-", uuid, NULL); + g_free(tmp_str); + nd = netplan_netdef_new(nd_id, nd_type, NETPLAN_BACKEND_NM); + + /* Handle uuid & NM name/id */ + nd->backend_settings.nm.uuid = g_strdup(uuid); + _kf_clear_key(kf, "connection", "uuid"); + nd->backend_settings.nm.name = g_key_file_get_string(kf, "connection", "id", NULL); + if (nd->backend_settings.nm.name) + _kf_clear_key(kf, "connection", "id"); + + if (nd_type == NETPLAN_DEF_TYPE_NM) + goto only_passthrough; //do not try to handle any keys for connections types unknown to netplan + + /* remove supported values from passthrough, which have been handled */ + if ( nd_type == NETPLAN_DEF_TYPE_ETHERNET + || nd_type == NETPLAN_DEF_TYPE_WIFI + || nd_type == NETPLAN_DEF_TYPE_MODEM + || nd_type == NETPLAN_DEF_TYPE_BRIDGE + || nd_type == NETPLAN_DEF_TYPE_BOND + || nd_type == NETPLAN_DEF_TYPE_VLAN) + _kf_clear_key(kf, "connection", "type"); + + /* Handle match: Netplan usually defines a connection per interface, while + * NM connection profiles are usually applied to any interface of matching + * type (like wifi/ethernet/...). */ + if (nd->type < NETPLAN_DEF_TYPE_VIRTUAL) { + nd->match.original_name = g_key_file_get_string(kf, "connection", "interface-name", NULL); + if (nd->match.original_name) + _kf_clear_key(kf, "connection", "interface-name"); + /* Set match, even if it is empty, so the NM renderer will not force + * the netdef ID as interface-name */ + nd->has_match = TRUE; + } + + /* DHCPv4/v6 */ + set_true_on_match(kf, "ipv4", "method", "auto", &nd->dhcp4); + set_true_on_match(kf, "ipv6", "method", "auto", &nd->dhcp6); + parse_dhcp_overrides(kf, "ipv4", &nd->dhcp4_overrides); + parse_dhcp_overrides(kf, "ipv6", &nd->dhcp6_overrides); + + /* Manual IPv4/6 addresses */ + parse_addresses(kf, "ipv4", &nd->ip4_addresses); + parse_addresses(kf, "ipv6", &nd->ip6_addresses); + + /* Default gateways */ + handle_generic_str(kf, "ipv4", "gateway", &nd->gateway4); + handle_generic_str(kf, "ipv6", "gateway", &nd->gateway6); + + /* Routes */ + parse_routes(kf, "ipv4", &nd->routes); + parse_routes(kf, "ipv6", &nd->routes); + + /* DNS: XXX: How to differentiate ip4/ip6 search_domains? */ + parse_search_domains(kf, "ipv4", &nd->search_domains); + parse_search_domains(kf, "ipv6", &nd->search_domains); + parse_nameservers(kf, "ipv4", &nd->ip4_nameservers); + parse_nameservers(kf, "ipv6", &nd->ip6_nameservers); + + /* IP6 addr-gen + * Different than suggested by the docs, NM stores 'addr-gen-mode' as string */ + tmp_str = g_key_file_get_string(kf, "ipv6", "addr-gen-mode", NULL); + if (tmp_str) { + if (g_strcmp0(tmp_str, "stable-privacy") == 0) { + nd->ip6_addr_gen_mode = NETPLAN_ADDRGEN_STABLEPRIVACY; + _kf_clear_key(kf, "ipv6", "addr-gen-mode"); + } else if (g_strcmp0(tmp_str, "eui64") == 0) { + nd->ip6_addr_gen_mode = NETPLAN_ADDRGEN_EUI64; + _kf_clear_key(kf, "ipv6", "addr-gen-mode"); + } + } + g_free(tmp_str); + handle_generic_str(kf, "ipv6", "token", &nd->ip6_addr_gen_token); + + /* Modem parameters + * NM differentiates between GSM and CDMA connections, while netplan + * combines them as "modems". We need to parse a basic set of parameters + * to enable the generator (in nm.c) to detect GSM vs CDMA connections, + * using its modem_is_gsm() util. */ + handle_generic_bool(kf, "gsm", "auto-config", &nd->modem_params.auto_config); + handle_generic_str(kf, "gsm", "apn", &nd->modem_params.apn); + handle_generic_str(kf, "gsm", "device-id", &nd->modem_params.device_id); + handle_generic_str(kf, "gsm", "network-id", &nd->modem_params.network_id); + handle_generic_str(kf, "gsm", "pin", &nd->modem_params.pin); + handle_generic_str(kf, "gsm", "sim-id", &nd->modem_params.sim_id); + handle_generic_str(kf, "gsm", "sim-operator-id", &nd->modem_params.sim_operator_id); + + /* GSM & CDMA */ + handle_generic_uint(kf, "cdma", "mtu", &nd->mtubytes, NETPLAN_MTU_UNSPEC); + handle_generic_uint(kf, "gsm", "mtu", &nd->mtubytes, NETPLAN_MTU_UNSPEC); + handle_generic_str(kf, "gsm", "number", &nd->modem_params.number); + if (!nd->modem_params.number) + handle_generic_str(kf, "cdma", "number", &nd->modem_params.number); + handle_generic_str(kf, "gsm", "password", &nd->modem_params.password); + if (!nd->modem_params.password) + handle_generic_str(kf, "cdma", "password", &nd->modem_params.password); + handle_generic_str(kf, "gsm", "username", &nd->modem_params.username); + if (!nd->modem_params.username) + handle_generic_str(kf, "cdma", "username", &nd->modem_params.username); + + /* Ethernets */ + if (g_key_file_has_group(kf, "ethernet")) { + /* wake-on-lan, do not clear passthrough as we do not fully support this setting */ + if (!g_key_file_has_key(kf, "ethernet", "wake-on-lan", NULL)) { + nd->wake_on_lan = TRUE; //NM's default is "1" + } else { + guint value = g_key_file_get_uint64(kf, "ethernet", "wake-on-lan", NULL); + //XXX: fix delta between options in NM (0x1, 0x2, 0x4, ...) and netplan (bool) + nd->wake_on_lan = value > 0; // netplan only knows about "off" or "on" + if (value == 0) + _kf_clear_key(kf, "ethernet", "wake-on-lan"); // value "off" is supported + } + + handle_common(kf, nd, "ethernet"); + } + + /* Wifis */ + if (g_key_file_has_group(kf, "wifi")) { + if (g_key_file_get_uint64(kf, "wifi", "wake-on-wlan", NULL)) { + nd->wowlan = g_key_file_get_uint64(kf, "wifi", "wake-on-wlan", NULL); + _kf_clear_key(kf, "wifi", "wake-on-wlan"); + } else { + nd->wowlan = NETPLAN_WIFI_WOWLAN_DEFAULT; + } + + handle_common(kf, nd, "wifi"); + } + + /* Cleanup some implicit keys */ + tmp_str = g_key_file_get_string(kf, "ipv6", "method", NULL); + if (tmp_str && g_strcmp0(tmp_str, "ignore") == 0 && + !(nd->dhcp6 || nd->ip6_addresses || nd->gateway6 || + nd->ip6_nameservers || nd->ip6_addr_gen_mode)) + _kf_clear_key(kf, "ipv6", "method"); + g_free(tmp_str); + + tmp_str = g_key_file_get_string(kf, "ipv4", "method", NULL); + if (tmp_str && g_strcmp0(tmp_str, "link-local") == 0 && + !(nd->dhcp4 || nd->ip4_addresses || nd->gateway4 || + nd->ip4_nameservers)) + _kf_clear_key(kf, "ipv4", "method"); + g_free(tmp_str); + + /* Vlan: XXX: find a way to parse the "link:" (parent) connection */ + handle_generic_uint(kf, "vlan", "id", &nd->vlan_id, G_MAXUINT); + + /* Bridge: XXX: find a way to parse the bridge-port.priority & bridge-port.path-cost values */ + handle_generic_uint(kf, "bridge", "priority", &nd->bridge_params.priority, 0); + if (nd->bridge_params.priority) + nd->custom_bridging = TRUE; + handle_bridge_uint(kf, "ageing-time", nd, &nd->bridge_params.ageing_time); + handle_bridge_uint(kf, "hello-time", nd, &nd->bridge_params.hello_time); + handle_bridge_uint(kf, "forward-delay", nd, &nd->bridge_params.forward_delay); + handle_bridge_uint(kf, "max-age", nd, &nd->bridge_params.max_age); + /* STP needs to be handled last, for its different default value in custom_bridging mode */ + if (g_key_file_has_key(kf, "bridge", "stp", NULL)) { + nd->custom_bridging = TRUE; + handle_generic_bool(kf, "bridge", "stp", &nd->bridge_params.stp); + } else if(nd->custom_bridging) { + nd->bridge_params.stp = TRUE; //set default value if not specified otherwise + } + + /* Bonds */ + handle_generic_str(kf, "bond", "mode", &nd->bond_params.mode); + handle_generic_str(kf, "bond", "lacp_rate", &nd->bond_params.lacp_rate); + handle_generic_str(kf, "bond", "miimon", &nd->bond_params.monitor_interval); + handle_generic_str(kf, "bond", "xmit_hash_policy", &nd->bond_params.transmit_hash_policy); + handle_generic_str(kf, "bond", "ad_select", &nd->bond_params.selection_logic); + handle_generic_str(kf, "bond", "arp_interval", &nd->bond_params.arp_interval); + handle_generic_str(kf, "bond", "arp_validate", &nd->bond_params.arp_validate); + handle_generic_str(kf, "bond", "arp_all_targets", &nd->bond_params.arp_all_targets); + handle_generic_str(kf, "bond", "updelay", &nd->bond_params.up_delay); + handle_generic_str(kf, "bond", "downdelay", &nd->bond_params.down_delay); + handle_generic_str(kf, "bond", "fail_over_mac", &nd->bond_params.fail_over_mac_policy); + handle_generic_str(kf, "bond", "primary_reselect", &nd->bond_params.primary_reselect_policy); + handle_generic_str(kf, "bond", "lp_interval", &nd->bond_params.learn_interval); + handle_generic_str(kf, "bond", "primary", &nd->bond_params.primary_slave); + handle_generic_uint(kf, "bond", "min_links", &nd->bond_params.min_links, 0); + handle_generic_uint(kf, "bond", "resend_igmp", &nd->bond_params.resend_igmp, 0); + handle_generic_uint(kf, "bond", "packets_per_slave", &nd->bond_params.packets_per_slave, 0); + handle_generic_uint(kf, "bond", "num_grat_arp", &nd->bond_params.gratuitous_arp, 0); + /* num_unsol_na might overwrite num_grat_arp, but we're fine if they are equal: + * https://github.com/NetworkManager/NetworkManager/commit/42b0bef33c77a0921590b2697f077e8ea7805166 */ + if (g_key_file_get_uint64(kf, "bond", "num_unsol_na", NULL) == nd->bond_params.gratuitous_arp) + _kf_clear_key(kf, "bond", "num_unsol_na"); + handle_generic_bool(kf, "bond", "all_slaves_active", &nd->bond_params.all_slaves_active); + parse_bond_arp_ip_targets(kf, &nd->bond_params.arp_ip_targets); + + /* Special handling for WiFi "access-points:" mapping */ + if (nd->type == NETPLAN_DEF_TYPE_WIFI) { + ap = g_new0(NetplanWifiAccessPoint, 1); + ap->ssid = g_key_file_get_string(kf, "wifi", "ssid", NULL); + if (!ap->ssid) { + g_warning("netplan: Keyfile: cannot find SSID for WiFi connection"); + return FALSE; + } else + _kf_clear_key(kf, "wifi", "ssid"); + + wifi_mode = g_key_file_get_string(kf, "wifi", "mode", NULL); + if (wifi_mode) { + ap->mode = ap_type_from_str(wifi_mode); + if (ap->mode != NETPLAN_WIFI_MODE_OTHER) + _kf_clear_key(kf, "wifi", "mode"); + } + + tmp_str = g_key_file_get_string(kf, "ipv4", "method", NULL); + if (tmp_str && g_strcmp0(tmp_str, "shared") == 0) { + ap->mode = NETPLAN_WIFI_MODE_AP; + _kf_clear_key(kf, "ipv4", "method"); + } + g_free(tmp_str); + + handle_generic_bool(kf, "wifi", "hidden", &ap->hidden); + handle_generic_str(kf, "wifi", "bssid", &ap->bssid); + + /* Wifi band & channel */ + tmp_str = g_key_file_get_string(kf, "wifi", "band", NULL); + if (tmp_str && g_strcmp0(tmp_str, "a") == 0) { + ap->band = NETPLAN_WIFI_BAND_5; + _kf_clear_key(kf, "wifi", "band"); + } else if (tmp_str && g_strcmp0(tmp_str, "bg") == 0) { + ap->band = NETPLAN_WIFI_BAND_24; + _kf_clear_key(kf, "wifi", "band"); + } + g_free(tmp_str); + handle_generic_uint(kf, "wifi", "channel", &ap->channel, 0); + + /* Wifi security */ + tmp_str = g_key_file_get_string(kf, "wifi-security", "key-mgmt", NULL); + if (tmp_str && g_strcmp0(tmp_str, "wpa-psk") == 0) { + ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK; + ap->has_auth = TRUE; + _kf_clear_key(kf, "wifi-security", "key-mgmt"); + } else if (tmp_str && g_strcmp0(tmp_str, "wpa-eap") == 0) { + ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP; + ap->has_auth = TRUE; + _kf_clear_key(kf, "wifi-security", "key-mgmt"); + } else if (tmp_str && g_strcmp0(tmp_str, "ieee8021x") == 0) { + ap->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_8021X; + ap->has_auth = TRUE; + _kf_clear_key(kf, "wifi-security", "key-mgmt"); + } + g_free(tmp_str); + + handle_generic_str(kf, "wifi-security", "psk", &ap->auth.password); + if (ap->auth.password) + ap->has_auth = TRUE; + + parse_dot1x_auth(kf, &ap->auth); + if (ap->auth.eap_method != NETPLAN_AUTH_EAP_NONE) + ap->has_auth = TRUE; + + if (!nd->access_points) + nd->access_points = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(nd->access_points, ap->ssid, ap); + + /* Last: handle passthrough for everything left in the keyfile + * Also, transfer backend_settings from netdef to AP */ + ap->backend_settings.nm.uuid = nd->backend_settings.nm.uuid; + ap->backend_settings.nm.name = nd->backend_settings.nm.name; + /* No need to clear nm.uuid & nm.name from def->backend_settings, + * as we have only one AP. */ + read_passthrough(kf, &ap->backend_settings.nm.passthrough); + } else { +only_passthrough: + /* Last: handle passthrough for everything left in the keyfile */ + read_passthrough(kf, &nd->backend_settings.nm.passthrough); + } + + g_key_file_free(kf); + return TRUE; +} diff --git a/src/parse-nm.h b/src/parse-nm.h new file mode 100644 index 0000000..53973f7 --- /dev/null +++ b/src/parse-nm.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#define NETPLAN_NM_EMPTY_GROUP "_" + +gboolean netplan_parse_keyfile(const char* filename, GError** error); diff --git a/src/parse.c b/src/parse.c new file mode 100644 index 0000000..09cd1e2 --- /dev/null +++ b/src/parse.c @@ -0,0 +1,2790 @@ +/* + * Copyright (C) 2016 Canonical, Ltd. + * Author: Martin Pitt + * Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "parse.h" +#include "util.h" +#include "error.h" +#include "validation.h" + +/* convenience macro to put the offset of a NetplanNetDefinition field into "void* data" */ +#define access_point_offset(field) GUINT_TO_POINTER(offsetof(NetplanWifiAccessPoint, field)) +#define addr_option_offset(field) GUINT_TO_POINTER(offsetof(NetplanAddressOptions, field)) +#define auth_offset(field) GUINT_TO_POINTER(offsetof(NetplanAuthenticationSettings, field)) +#define ip_rule_offset(field) GUINT_TO_POINTER(offsetof(NetplanIPRule, field)) +#define netdef_offset(field) GUINT_TO_POINTER(offsetof(NetplanNetDefinition, field)) +#define ovs_settings_offset(field) GUINT_TO_POINTER(offsetof(NetplanOVSSettings, field)) +#define route_offset(field) GUINT_TO_POINTER(offsetof(NetplanIPRoute, field)) +#define wireguard_peer_offset(field) GUINT_TO_POINTER(offsetof(NetplanWireguardPeer, field)) + +/* convenience macro to avoid strdup'ing a string into a field if it's already set. */ +#define set_str_if_null(dst, src) { if (dst) {\ + g_assert_cmpstr(src, ==, dst); \ +} else { \ + dst = g_strdup(src); \ +} } + +/* NetplanNetDefinition that is currently being processed */ +static NetplanNetDefinition* cur_netdef; + +/* NetplanWifiAccessPoint that is currently being processed */ +static NetplanWifiAccessPoint* cur_access_point; + +/* NetplanAuthenticationSettings that are currently being processed */ +static NetplanAuthenticationSettings* cur_auth; + +/* NetplanWireguardPeer that is currently being processed */ +static NetplanWireguardPeer* cur_wireguard_peer; + +static NetplanAddressOptions* cur_addr_option; + +static NetplanIPRoute* cur_route; +static NetplanIPRule* cur_ip_rule; + +/* Filename of the currently parsed YAML file */ +const char* cur_filename; + +static NetplanBackend backend_global, backend_cur_type; + +/* global OpenVSwitch settings */ +NetplanOVSSettings ovs_settings_global; + +/* Global ID → NetplanNetDefinition* map for all parsed config files */ +GHashTable* netdefs; + +/* Contains the same objects as 'netdefs' but ordered by dependency */ +GList* netdefs_ordered; + +/* Set of IDs in currently parsed YAML file, for being able to detect + * "duplicate ID within one file" vs. allowing a drop-in to override/amend an + * existing definition */ +static GHashTable* ids_in_file; + +/* Global variables, defined in this file */ +int missing_ids_found; +const char* current_file; +GHashTable* missing_id; + +/** + * Load YAML file name into a yaml_document_t. + * + * Returns: TRUE on success, FALSE if the document is malformed; @error gets set then. + */ +static gboolean +load_yaml(const char* yaml, yaml_document_t* doc, GError** error) +{ + FILE* fyaml = NULL; + yaml_parser_t parser; + gboolean ret = TRUE; + + current_file = yaml; + + fyaml = g_fopen(yaml, "r"); + if (!fyaml) { + g_set_error(error, G_FILE_ERROR, errno, "Cannot open %s: %s", yaml, g_strerror(errno)); + return FALSE; + } + + yaml_parser_initialize(&parser); + yaml_parser_set_input_file(&parser, fyaml); + if (!yaml_parser_load(&parser, doc)) { + ret = parser_error(&parser, yaml, error); + } + + yaml_parser_delete(&parser); + fclose(fyaml); + return ret; +} + +#define YAML_VARIABLE_NODE YAML_NO_NODE + +/** + * Raise a GError about a type mismatch and return FALSE. + */ +static gboolean +assert_type_fn(yaml_node_t* node, yaml_node_type_t expected_type, GError** error) +{ + if (node->type == expected_type) + return TRUE; + + switch (expected_type) { + case YAML_VARIABLE_NODE: + /* Special case, defer sanity checking to the next handlers */ + return TRUE; + break; + case YAML_SCALAR_NODE: + yaml_error(node, error, "expected scalar"); + break; + case YAML_SEQUENCE_NODE: + yaml_error(node, error, "expected sequence"); + break; + case YAML_MAPPING_NODE: + yaml_error(node, error, "expected mapping (check indentation)"); + break; + + // LCOV_EXCL_START + default: + g_assert_not_reached(); + // LCOV_EXCL_STOP + } + return FALSE; +} + +#define assert_type(n,t) { if (!assert_type_fn(n,t,error)) return FALSE; } + +static inline const char* +scalar(const yaml_node_t* node) +{ + return (const char*) node->data.scalar.value; +} + +static void +add_missing_node(const yaml_node_t* node) +{ + NetplanMissingNode* missing; + + /* Let's capture the current netdef we were playing with along with the + * actual yaml_node_t that errors (that is an identifier not previously + * seen by the compiler). We can use it later to write an sensible error + * message and point the user in the right direction. */ + missing = g_new0(NetplanMissingNode, 1); + missing->netdef_id = cur_netdef->id; + missing->node = node; + + g_debug("recording missing yaml_node_t %s", scalar(node)); + g_hash_table_insert(missing_id, (gpointer)scalar(node), missing); +} + +/** + * Check that node contains a valid ID/interface name. Raise GError if not. + */ +static gboolean +assert_valid_id(yaml_node_t* node, GError** error) +{ + static regex_t re; + static gboolean re_inited = FALSE; + + assert_type(node, YAML_SCALAR_NODE); + + if (!re_inited) { + g_assert(regcomp(&re, "^[[:alnum:][:punct:]]+$", REG_EXTENDED|REG_NOSUB) == 0); + re_inited = TRUE; + } + + if (regexec(&re, scalar(node), 0, NULL, 0) != 0) + return yaml_error(node, error, "Invalid name '%s'", scalar(node)); + return TRUE; +} + +static void +initialize_dhcp_overrides(NetplanDHCPOverrides* overrides) +{ + overrides->use_dns = TRUE; + overrides->use_domains = NULL; + overrides->use_ntp = TRUE; + overrides->send_hostname = TRUE; + overrides->use_hostname = TRUE; + overrides->use_mtu = TRUE; + overrides->use_routes = TRUE; + overrides->hostname = NULL; + overrides->metric = NETPLAN_METRIC_UNSPEC; +} + +static void +initialize_ovs_settings(NetplanOVSSettings* ovs_settings) +{ + ovs_settings->mcast_snooping = FALSE; + ovs_settings->rstp = FALSE; +} + +NetplanNetDefinition* +netplan_netdef_new(const char* id, NetplanDefType type, NetplanBackend backend) +{ + /* create new network definition */ + cur_netdef = g_new0(NetplanNetDefinition, 1); + cur_netdef->type = type; + cur_netdef->backend = backend ?: NETPLAN_BACKEND_NONE; + cur_netdef->id = g_strdup(id); + + /* Set some default values */ + cur_netdef->vlan_id = G_MAXUINT; /* 0 is a valid ID */ + cur_netdef->tunnel.mode = NETPLAN_TUNNEL_MODE_UNKNOWN; + cur_netdef->dhcp_identifier = g_strdup("duid"); /* keep networkd's default */ + /* systemd-networkd defaults to IPv6 LL enabled; keep that default */ + cur_netdef->linklocal.ipv6 = TRUE; + cur_netdef->sriov_vlan_filter = FALSE; + cur_netdef->sriov_explicit_vf_count = G_MAXUINT; /* 0 is a valid number of VFs */ + + /* DHCP override defaults */ + initialize_dhcp_overrides(&cur_netdef->dhcp4_overrides); + initialize_dhcp_overrides(&cur_netdef->dhcp6_overrides); + + /* OpenVSwitch defaults */ + initialize_ovs_settings(&cur_netdef->ovs_settings); + + if (!netdefs) + netdefs = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(netdefs, cur_netdef->id, cur_netdef); + netdefs_ordered = g_list_append(netdefs_ordered, cur_netdef); + return cur_netdef; +} + +/**************************************************** + * Data types and functions for interpreting YAML nodes + ****************************************************/ + +typedef gboolean (*node_handler) (yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error); + +typedef struct mapping_entry_handler_s { + /* mapping key (must be scalar) */ + const char* key; + /* expected type of the mapped value */ + yaml_node_type_t type; + /* handler for the value of this key */ + node_handler handler; + /* if type == YAML_MAPPING_NODE and handler is NULL, use process_mapping() + * on this handler map as handler */ + const struct mapping_entry_handler_s* map_handlers; + /* user_data */ + const void* data; +} mapping_entry_handler; + +/** + * Return the #mapping_entry_handler that matches @key, or NULL if not found. + */ +static const mapping_entry_handler* +get_handler(const mapping_entry_handler* handlers, const char* key) +{ + for (unsigned i = 0; handlers[i].key != NULL; ++i) { + if (g_strcmp0(handlers[i].key, key) == 0) + return &handlers[i]; + } + return NULL; +} + +/** + * Call handlers for all entries in a YAML mapping. + * @doc: The yaml_document_t + * @node: The yaml_node_t to process, must be a #YAML_MAPPING_NODE + * @handlers: Array of mapping_entry_handler with allowed keys + * @error: Gets set on data type errors or unknown keys + * + * Returns: TRUE on success, FALSE on error (@error gets set then). + */ +static gboolean +process_mapping(yaml_document_t* doc, yaml_node_t* node, const mapping_entry_handler* handlers, GList** out_values, GError** error) +{ + yaml_node_pair_t* entry; + + assert_type(node, YAML_MAPPING_NODE); + + for (entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + const mapping_entry_handler* h; + + g_assert(error == NULL || *error == NULL); + + key = yaml_document_get_node(doc, entry->key); + value = yaml_document_get_node(doc, entry->value); + assert_type(key, YAML_SCALAR_NODE); + h = get_handler(handlers, scalar(key)); + if (!h) + return yaml_error(key, error, "unknown key '%s'", scalar(key)); + assert_type(value, h->type); + if (out_values) + *out_values = g_list_prepend(*out_values, g_strdup(scalar(key))); + if (h->map_handlers) { + g_assert(h->handler == NULL); + g_assert(h->type == YAML_MAPPING_NODE); + if (!process_mapping(doc, value, h->map_handlers, NULL, error)) + return FALSE; + } else { + if (!h->handler(doc, value, h->data, error)) + return FALSE; + } + } + + return TRUE; +} + +/************************************************************* + * Generic helper functions to extract data from scalar nodes. + *************************************************************/ + +/** + * Handler for setting a guint field from a scalar node, inside a given struct + * @entryptr: pointer to the begining of the to-be-modified data structure + * @data: offset into entryptr struct where the guint field to write is located + */ +static gboolean +handle_generic_guint(yaml_document_t* doc, yaml_node_t* node, const void* entryptr, const void* data, GError** error) +{ + g_assert(entryptr); + guint offset = GPOINTER_TO_UINT(data); + guint64 v; + gchar* endptr; + + v = g_ascii_strtoull(scalar(node), &endptr, 10); + if (*endptr != '\0' || v > G_MAXUINT) + return yaml_error(node, error, "invalid unsigned int value '%s'", scalar(node)); + + *((guint*) ((void*) entryptr + offset)) = (guint) v; + return TRUE; +} + +/** + * Handler for setting a string field from a scalar node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the const char* field to write is + * located + */ +static gboolean +handle_generic_str(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + g_assert(entryptr); + guint offset = GPOINTER_TO_UINT(data); + char** dest = (char**) ((void*) entryptr + offset); + g_free(*dest); + *dest = g_strdup(scalar(node)); + return TRUE; +} + +/* + * Handler for setting a MAC address field from a scalar node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the const char* field to write is + * located + */ +static gboolean +handle_generic_mac(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + g_assert(entryptr); + static regex_t re; + static gboolean re_inited = FALSE; + + g_assert(node->type == YAML_SCALAR_NODE); + + if (!re_inited) { + g_assert(regcomp(&re, "^[[:xdigit:]][[:xdigit:]]:[[:xdigit:]][[:xdigit:]]:[[:xdigit:]][[:xdigit:]]:[[:xdigit:]][[:xdigit:]]:[[:xdigit:]][[:xdigit:]]:[[:xdigit:]][[:xdigit:]]$", REG_EXTENDED|REG_NOSUB) == 0); + re_inited = TRUE; + } + + if (regexec(&re, scalar(node), 0, NULL, 0) != 0) + return yaml_error(node, error, "Invalid MAC address '%s', must be XX:XX:XX:XX:XX:XX", scalar(node)); + + return handle_generic_str(doc, node, entryptr, data, error); +} + +/* + * Handler for setting a boolean field from a scalar node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the boolean field to write is located + */ +static gboolean +handle_generic_bool(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + g_assert(entryptr); + guint offset = GPOINTER_TO_UINT(data); + gboolean v; + + if (g_ascii_strcasecmp(scalar(node), "true") == 0 || + g_ascii_strcasecmp(scalar(node), "on") == 0 || + g_ascii_strcasecmp(scalar(node), "yes") == 0 || + g_ascii_strcasecmp(scalar(node), "y") == 0) + v = TRUE; + else if (g_ascii_strcasecmp(scalar(node), "false") == 0 || + g_ascii_strcasecmp(scalar(node), "off") == 0 || + g_ascii_strcasecmp(scalar(node), "no") == 0 || + g_ascii_strcasecmp(scalar(node), "n") == 0) + v = FALSE; + else + return yaml_error(node, error, "invalid boolean value '%s'", scalar(node)); + + *((gboolean*) ((void*) entryptr + offset)) = v; + return TRUE; +} + +/* + * Handler for setting a HashTable field from a mapping node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the boolean field to write is located +*/ +static gboolean +handle_generic_map(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + GHashTable** map = (GHashTable**) ((void*) entryptr + offset); + if (!*map) + *map = g_hash_table_new(g_str_hash, g_str_equal); + + for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + + key = yaml_document_get_node(doc, entry->key); + value = yaml_document_get_node(doc, entry->value); + + assert_type(key, YAML_SCALAR_NODE); + assert_type(value, YAML_SCALAR_NODE); + + /* TODO: make sure we free all the memory here */ + if (!g_hash_table_insert(*map, g_strdup(scalar(key)), g_strdup(scalar(value)))) + return yaml_error(node, error, "duplicate map entry '%s'", scalar(key)); + } + + return TRUE; +} + +/* + * Handler for setting a DataList field from a mapping node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the boolean field to write is located +*/ +static gboolean +handle_generic_datalist(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + GData** list = (GData**) ((void*) entryptr + offset); + if (!*list) + g_datalist_init(list); + + for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + + key = yaml_document_get_node(doc, entry->key); + value = yaml_document_get_node(doc, entry->value); + + assert_type(key, YAML_SCALAR_NODE); + assert_type(value, YAML_SCALAR_NODE); + + g_datalist_set_data_full(list, g_strdup(scalar(key)), g_strdup(scalar(value)), g_free); + } + + return TRUE; +} + +/** + * Generic handler for setting a cur_netdef string field from a scalar node + * @data: offset into NetplanNetDefinition where the const char* field to write is + * located + */ +static gboolean +handle_netdef_str(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_str(doc, node, cur_netdef, data, error); +} + +/** + * Generic handler for setting a cur_netdef ID/iface name field from a scalar node + * @data: offset into NetplanNetDefinition where the const char* field to write is + * located + */ +static gboolean +handle_netdef_id(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (!assert_valid_id(node, error)) + return FALSE; + return handle_netdef_str(doc, node, data, error); +} + +/** + * Generic handler for setting a cur_netdef ID/iface name field referring to an + * existing ID from a scalar node. This handler also includes a special case + * handler for OVS VLANs, switching the backend implicitly to OVS for such + * interfaces + * @data: offset into NetplanNetDefinition where the NetplanNetDefinition* field to write is + * located + */ +static gboolean +handle_netdef_id_ref(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + NetplanNetDefinition* ref = NULL; + + ref = g_hash_table_lookup(netdefs, scalar(node)); + if (!ref) { + add_missing_node(node); + } else { + *((NetplanNetDefinition**) ((void*) cur_netdef + offset)) = ref; + + if (cur_netdef->type == NETPLAN_DEF_TYPE_VLAN && ref->backend == NETPLAN_BACKEND_OVS) { + g_debug("%s: VLAN defined for openvswitch interface, choosing OVS backend", cur_netdef->id); + cur_netdef->backend = NETPLAN_BACKEND_OVS; + } + } + return TRUE; +} + + +/** + * Generic handler for setting a cur_netdef MAC address field from a scalar node + * @data: offset into NetplanNetDefinition where the const char* field to write is + * located + */ +static gboolean +handle_netdef_mac(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_mac(doc, node, cur_netdef, data, error); +} + +/** + * Generic handler for setting a cur_netdef gboolean field from a scalar node + * @data: offset into NetplanNetDefinition where the gboolean field to write is located + */ +static gboolean +handle_netdef_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_bool(doc, node, cur_netdef, data, error); +} + +/** + * Generic handler for setting a cur_netdef guint field from a scalar node + * @data: offset into NetplanNetDefinition where the guint field to write is located + */ +static gboolean +handle_netdef_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_guint(doc, node, cur_netdef, data, error); +} + +static gboolean +handle_netdef_ip4(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + char** dest = (char**) ((void*) cur_netdef + offset); + g_autofree char* addr = NULL; + char* prefix_len; + + /* these addresses can't have /prefix_len */ + addr = g_strdup(scalar(node)); + prefix_len = strrchr(addr, '/'); + + /* FIXME: stop excluding this from coverage; refactor address handling instead */ + // LCOV_EXCL_START + if (prefix_len) + return yaml_error(node, error, + "invalid address: a single IPv4 address (without /prefixlength) is required"); + + /* is it an IPv4 address? */ + if (!is_ip4_address(addr)) + return yaml_error(node, error, + "invalid IPv4 address: %s", scalar(node)); + // LCOV_EXCL_STOP + + g_free(*dest); + *dest = g_strdup(scalar(node)); + + return TRUE; +} + +static gboolean +handle_netdef_ip6(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + char** dest = (char**) ((void*) cur_netdef + offset); + g_autofree char* addr = NULL; + char* prefix_len; + + /* these addresses can't have /prefix_len */ + addr = g_strdup(scalar(node)); + prefix_len = strrchr(addr, '/'); + + /* FIXME: stop excluding this from coverage; refactor address handling instead */ + // LCOV_EXCL_START + if (prefix_len) + return yaml_error(node, error, + "invalid address: a single IPv6 address (without /prefixlength) is required"); + + /* is it an IPv6 address? */ + if (!is_ip6_address(addr)) + return yaml_error(node, error, + "invalid IPv6 address: %s", scalar(node)); + // LCOV_EXCL_STOP + + g_free(*dest); + *dest = g_strdup(scalar(node)); + + return TRUE; +} + +static gboolean +handle_netdef_addrgen(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_assert(cur_netdef); + if (strcmp(scalar(node), "eui64") == 0) + cur_netdef->ip6_addr_gen_mode = NETPLAN_ADDRGEN_EUI64; + else if (strcmp(scalar(node), "stable-privacy") == 0) + cur_netdef->ip6_addr_gen_mode = NETPLAN_ADDRGEN_STABLEPRIVACY; + else + return yaml_error(node, error, "unknown ipv6-address-generation '%s'", scalar(node)); + return TRUE; +} + +static gboolean +handle_netdef_addrtok(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_netdef); + gboolean ret = handle_netdef_str(doc, node, data, error); + if (!is_ip6_address(cur_netdef->ip6_addr_gen_token)) + return yaml_error(node, error, "invalid ipv6-address-token '%s'", scalar(node)); + return ret; +} + +static gboolean +handle_netdef_map(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_netdef); + return handle_generic_map(doc, node, cur_netdef, data, error); +} + +static gboolean +handle_netdef_datalist(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_netdef); + return handle_generic_datalist(doc, node, cur_netdef, data, error); +} + +/**************************************************** + * Grammar and handlers for network config "match" entry + ****************************************************/ + +static const mapping_entry_handler match_handlers[] = { + {"driver", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(match.driver)}, + {"macaddress", YAML_SCALAR_NODE, handle_netdef_mac, NULL, netdef_offset(match.mac)}, + {"name", YAML_SCALAR_NODE, handle_netdef_id, NULL, netdef_offset(match.original_name)}, + {NULL} +}; + +/**************************************************** + * Grammar and handlers for network config "auth" entry + ****************************************************/ + +static gboolean +handle_auth_str(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_auth); + guint offset = GPOINTER_TO_UINT(data); + char** dest = (char**) ((void*) cur_auth + offset); + g_free(*dest); + *dest = g_strdup(scalar(node)); + return TRUE; +} + +static gboolean +handle_auth_key_management(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_assert(cur_auth); + if (strcmp(scalar(node), "none") == 0) + cur_auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_NONE; + else if (strcmp(scalar(node), "psk") == 0) + cur_auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK; + else if (strcmp(scalar(node), "eap") == 0) + cur_auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP; + else if (strcmp(scalar(node), "802.1x") == 0) + cur_auth->key_management = NETPLAN_AUTH_KEY_MANAGEMENT_8021X; + else + return yaml_error(node, error, "unknown key management type '%s'", scalar(node)); + return TRUE; +} + +static gboolean +handle_auth_method(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_assert(cur_auth); + if (strcmp(scalar(node), "tls") == 0) + cur_auth->eap_method = NETPLAN_AUTH_EAP_TLS; + else if (strcmp(scalar(node), "peap") == 0) + cur_auth->eap_method = NETPLAN_AUTH_EAP_PEAP; + else if (strcmp(scalar(node), "ttls") == 0) + cur_auth->eap_method = NETPLAN_AUTH_EAP_TTLS; + else + return yaml_error(node, error, "unknown EAP method '%s'", scalar(node)); + return TRUE; +} + +static const mapping_entry_handler auth_handlers[] = { + {"key-management", YAML_SCALAR_NODE, handle_auth_key_management}, + {"method", YAML_SCALAR_NODE, handle_auth_method}, + {"identity", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(identity)}, + {"anonymous-identity", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(anonymous_identity)}, + {"password", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(password)}, + {"ca-certificate", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(ca_certificate)}, + {"client-certificate", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_certificate)}, + {"client-key", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_key)}, + {"client-key-password", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_key_password)}, + {"phase2-auth", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(phase2_auth)}, + {NULL} +}; + +/**************************************************** + * Grammar and handlers for network device definition + ****************************************************/ + +static NetplanBackend +get_default_backend_for_type(NetplanDefType type) +{ + if (backend_global != NETPLAN_BACKEND_NONE) + return backend_global; + + /* networkd can handle all device types at the moment, so nothing + * type-specific */ + return NETPLAN_BACKEND_NETWORKD; +} + +static gboolean +handle_access_point_str(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_str(doc, node, cur_access_point, data, error); +} + +static gboolean +handle_access_point_datalist(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_access_point); + return handle_generic_datalist(doc, node, cur_access_point, data, error); +} + +static gboolean +handle_access_point_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_guint(doc, node, cur_access_point, data, error); +} + +static gboolean +handle_access_point_mac(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_mac(doc, node, cur_access_point, data, error); +} + +static gboolean +handle_access_point_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_bool(doc, node, cur_access_point, data, error); +} + +static gboolean +handle_access_point_password(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_assert(cur_access_point); + /* shortcut for WPA-PSK */ + cur_access_point->has_auth = TRUE; + cur_access_point->auth.key_management = NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK; + g_free(cur_access_point->auth.password); + cur_access_point->auth.password = g_strdup(scalar(node)); + return TRUE; +} + +static gboolean +handle_access_point_auth(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ret; + + g_assert(cur_access_point); + cur_access_point->has_auth = TRUE; + + cur_auth = &cur_access_point->auth; + ret = process_mapping(doc, node, auth_handlers, NULL, error); + cur_auth = NULL; + + return ret; +} + +static gboolean +handle_access_point_mode(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_assert(cur_access_point); + if (strcmp(scalar(node), "infrastructure") == 0) + cur_access_point->mode = NETPLAN_WIFI_MODE_INFRASTRUCTURE; + else if (strcmp(scalar(node), "adhoc") == 0) + cur_access_point->mode = NETPLAN_WIFI_MODE_ADHOC; + else if (strcmp(scalar(node), "ap") == 0) + cur_access_point->mode = NETPLAN_WIFI_MODE_AP; + else + return yaml_error(node, error, "unknown wifi mode '%s'", scalar(node)); + return TRUE; +} + +static gboolean +handle_access_point_band(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_assert(cur_access_point); + if (strcmp(scalar(node), "5GHz") == 0 || strcmp(scalar(node), "5G") == 0) + cur_access_point->band = NETPLAN_WIFI_BAND_5; + else if (strcmp(scalar(node), "2.4GHz") == 0 || strcmp(scalar(node), "2.4G") == 0) + cur_access_point->band = NETPLAN_WIFI_BAND_24; + else + return yaml_error(node, error, "unknown wifi band '%s'", scalar(node)); + return TRUE; +} + +/* Keep in sync with ap_nm_backend_settings_handlers */ +static const mapping_entry_handler nm_backend_settings_handlers[] = { + {"name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.name)}, + {"uuid", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.uuid)}, + {"stable-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.stable_id)}, + {"device", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.device)}, + /* Fallback mode, to support all NM settings of the NetworkManager netplan backend */ + {"passthrough", YAML_MAPPING_NODE, handle_netdef_datalist, NULL, netdef_offset(backend_settings.nm.passthrough)}, + {NULL} +}; + +/* Keep in sync with nm_backend_settings_handlers */ +static const mapping_entry_handler ap_nm_backend_settings_handlers[] = { + {"name", YAML_SCALAR_NODE, handle_access_point_str, NULL, access_point_offset(backend_settings.nm.name)}, + {"uuid", YAML_SCALAR_NODE, handle_access_point_str, NULL, access_point_offset(backend_settings.nm.uuid)}, + {"stable-id", YAML_SCALAR_NODE, handle_access_point_str, NULL, access_point_offset(backend_settings.nm.stable_id)}, + {"device", YAML_SCALAR_NODE, handle_access_point_str, NULL, access_point_offset(backend_settings.nm.device)}, + /* Fallback mode, to support all NM settings of the NetworkManager netplan backend */ + {"passthrough", YAML_MAPPING_NODE, handle_access_point_datalist, NULL, access_point_offset(backend_settings.nm.passthrough)}, + {NULL} +}; + + +static const mapping_entry_handler wifi_access_point_handlers[] = { + {"band", YAML_SCALAR_NODE, handle_access_point_band}, + {"bssid", YAML_SCALAR_NODE, handle_access_point_mac, NULL, access_point_offset(bssid)}, + {"hidden", YAML_SCALAR_NODE, handle_access_point_bool, NULL, access_point_offset(hidden)}, + {"channel", YAML_SCALAR_NODE, handle_access_point_guint, NULL, access_point_offset(channel)}, + {"mode", YAML_SCALAR_NODE, handle_access_point_mode}, + {"password", YAML_SCALAR_NODE, handle_access_point_password}, + {"auth", YAML_MAPPING_NODE, handle_access_point_auth}, + {"networkmanager", YAML_MAPPING_NODE, NULL, ap_nm_backend_settings_handlers}, + {NULL} +}; + +/** + * Parse scalar node's string into a netdef_backend. + */ +static gboolean +parse_renderer(yaml_node_t* node, NetplanBackend* backend, GError** error) +{ + if (strcmp(scalar(node), "networkd") == 0) + *backend = NETPLAN_BACKEND_NETWORKD; + else if (strcmp(scalar(node), "NetworkManager") == 0) + *backend = NETPLAN_BACKEND_NM; + else + return yaml_error(node, error, "unknown renderer '%s'", scalar(node)); + return TRUE; +} + +static gboolean +handle_netdef_renderer(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + if (cur_netdef->type == NETPLAN_DEF_TYPE_VLAN) { + if (strcmp(scalar(node), "sriov") == 0) { + cur_netdef->sriov_vlan_filter = TRUE; + return TRUE; + } + } + + return parse_renderer(node, &cur_netdef->backend, error); +} + +static gboolean +handle_accept_ra(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + gboolean ret = handle_generic_bool(doc, node, cur_netdef, data, error); + if (cur_netdef->accept_ra) + cur_netdef->accept_ra = NETPLAN_RA_MODE_ENABLED; + else + cur_netdef->accept_ra = NETPLAN_RA_MODE_DISABLED; + return ret; +} + +static gboolean +handle_activation_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (g_strcmp0(scalar(node), "manual") && g_strcmp0(scalar(node), "off")) + return yaml_error(node, error, "Value of 'activation-mode' needs to be 'manual' or 'off'"); + + return handle_netdef_str(doc, node, data, error); +} + +static gboolean +handle_match(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + cur_netdef->has_match = TRUE; + return process_mapping(doc, node, match_handlers, NULL, error); +} + +struct NetplanWifiWowlanType NETPLAN_WIFI_WOWLAN_TYPES[] = { + {"default", NETPLAN_WIFI_WOWLAN_DEFAULT}, + {"any", NETPLAN_WIFI_WOWLAN_ANY}, + {"disconnect", NETPLAN_WIFI_WOWLAN_DISCONNECT}, + {"magic_pkt", NETPLAN_WIFI_WOWLAN_MAGIC}, + {"gtk_rekey_failure", NETPLAN_WIFI_WOWLAN_GTK_REKEY_FAILURE}, + {"eap_identity_req", NETPLAN_WIFI_WOWLAN_EAP_IDENTITY_REQ}, + {"four_way_handshake", NETPLAN_WIFI_WOWLAN_4WAY_HANDSHAKE}, + {"rfkill_release", NETPLAN_WIFI_WOWLAN_RFKILL_RELEASE}, + {"tcp", NETPLAN_WIFI_WOWLAN_TCP}, + {NULL}, +}; + +static gboolean +handle_wowlan(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + int found = FALSE; + + for (unsigned i = 0; NETPLAN_WIFI_WOWLAN_TYPES[i].name != NULL; ++i) { + if (g_ascii_strcasecmp(scalar(entry), NETPLAN_WIFI_WOWLAN_TYPES[i].name) == 0) { + cur_netdef->wowlan |= NETPLAN_WIFI_WOWLAN_TYPES[i].flag; + found = TRUE; + break; + } + } + if (!found) + return yaml_error(node, error, "invalid value for wakeonwlan: '%s'", scalar(entry)); + } + if (cur_netdef->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT && cur_netdef->wowlan & NETPLAN_WIFI_WOWLAN_TYPES[0].flag) + return yaml_error(node, error, "'default' is an exclusive flag for wakeonwlan"); + return TRUE; +} + +static gboolean +handle_auth(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ret; + + cur_netdef->has_auth = TRUE; + + cur_auth = &cur_netdef->auth; + ret = process_mapping(doc, node, auth_handlers, NULL, error); + cur_auth = NULL; + + return ret; +} + +static gboolean +handle_address_option_lifetime(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (g_ascii_strcasecmp(scalar(node), "0") != 0 && + g_ascii_strcasecmp(scalar(node), "forever") != 0) { + return yaml_error(node, error, "invalid lifetime value '%s'", scalar(node)); + } + return handle_generic_str(doc, node, cur_addr_option, data, error); +} + +static gboolean +handle_address_option_label(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_str(doc, node, cur_addr_option, data, error); +} + +const mapping_entry_handler address_option_handlers[] = { + {"lifetime", YAML_SCALAR_NODE, handle_address_option_lifetime, NULL, addr_option_offset(lifetime)}, + {"label", YAML_SCALAR_NODE, handle_address_option_label, NULL, addr_option_offset(label)}, + {NULL} +}; + +/* + * Handler for setting an array of IP addresses from a sequence node, inside a given struct + * @entryptr: pointer to the beginning of the do-be-modified data structure + * @data: offset into entryptr struct where the array to write is located + */ +static gboolean +handle_generic_addresses(yaml_document_t* doc, yaml_node_t* node, gboolean check_zero_prefix, GArray** ip4, GArray** ip6, GError** error) +{ + g_assert(ip4); + g_assert(ip6); + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + g_autofree char* addr = NULL; + char* prefix_len; + guint64 prefix_len_num; + yaml_node_t *entry = yaml_document_get_node(doc, *i); + yaml_node_t *key = NULL; + yaml_node_t *value = NULL; + + if (entry->type != YAML_SCALAR_NODE && entry->type != YAML_MAPPING_NODE) { + return yaml_error(entry, error, "expected either scalar or mapping (check indentation)"); + } + + if (entry->type == YAML_MAPPING_NODE) { + key = yaml_document_get_node(doc, entry->data.mapping.pairs.start->key); + value = yaml_document_get_node(doc, entry->data.mapping.pairs.start->value); + entry = key; + } + assert_type(entry, YAML_SCALAR_NODE); + + /* split off /prefix_len */ + addr = g_strdup(scalar(entry)); + prefix_len = strrchr(addr, '/'); + if (!prefix_len) + return yaml_error(node, error, "address '%s' is missing /prefixlength", scalar(entry)); + *prefix_len = '\0'; + prefix_len++; /* skip former '/' into first char of prefix */ + prefix_len_num = g_ascii_strtoull(prefix_len, NULL, 10); + + if (value) { + if (!is_ip4_address(addr) && !is_ip6_address(addr)) + return yaml_error(node, error, "malformed address '%s', must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", scalar(entry)); + + if (!cur_netdef->address_options) + cur_netdef->address_options = g_array_new(FALSE, FALSE, sizeof(NetplanAddressOptions*)); + + for (unsigned i = 0; i < cur_netdef->address_options->len; ++i) { + NetplanAddressOptions* opts = g_array_index(cur_netdef->address_options, NetplanAddressOptions*, i); + /* check for multi-pass parsing, return early if options for this address already exist */ + if (!g_strcmp0(scalar(key), opts->address)) + return TRUE; + } + + cur_addr_option = g_new0(NetplanAddressOptions, 1); + cur_addr_option->address = g_strdup(scalar(key)); + + if (!process_mapping(doc, value, address_option_handlers, NULL, error)) + return FALSE; + + g_array_append_val(cur_netdef->address_options, cur_addr_option); + continue; + } + + /* is it an IPv4 address? */ + if (is_ip4_address(addr)) { + if ((check_zero_prefix && prefix_len_num == 0) || prefix_len_num > 32) + return yaml_error(node, error, "invalid prefix length in address '%s'", scalar(entry)); + + if (!*ip4) + *ip4 = g_array_new(FALSE, FALSE, sizeof(char*)); + + /* Do not append the same IP (on multiple passes), if it is already contained */ + for (unsigned i = 0; i < (*ip4)->len; ++i) + if (!g_strcmp0(scalar(entry), g_array_index(*ip4, char*, i))) + goto skip_ip4; + char* s = g_strdup(scalar(entry)); + g_array_append_val(*ip4, s); +skip_ip4: + continue; + } + + /* is it an IPv6 address? */ + if (is_ip6_address(addr)) { + if ((check_zero_prefix && prefix_len_num == 0) || prefix_len_num > 128) + return yaml_error(node, error, "invalid prefix length in address '%s'", scalar(entry)); + if (!*ip6) + *ip6 = g_array_new(FALSE, FALSE, sizeof(char*)); + + /* Do not append the same IP (on multiple passes), if it is already contained */ + for (unsigned i = 0; i < (*ip6)->len; ++i) + if (!g_strcmp0(scalar(entry), g_array_index(*ip6, char*, i))) + goto skip_ip6; + char* s = g_strdup(scalar(entry)); + g_array_append_val(*ip6, s); +skip_ip6: + continue; + } + + return yaml_error(node, error, "malformed address '%s', must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", scalar(entry)); + } + + return TRUE; +} + +static gboolean +handle_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + return handle_generic_addresses(doc, node, TRUE, &(cur_netdef->ip4_addresses), &(cur_netdef->ip6_addresses), error); +} + +static gboolean +handle_gateway4(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + if (!is_ip4_address(scalar(node))) + return yaml_error(node, error, "invalid IPv4 address '%s'", scalar(node)); + set_str_if_null(cur_netdef->gateway4, scalar(node)); + g_warning("`gateway4` has been deprecated, use default routes instead.\n" + "See the 'Default routes' section of the documentation for more details."); + return TRUE; +} + +static gboolean +handle_gateway6(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + if (!is_ip6_address(scalar(node))) + return yaml_error(node, error, "invalid IPv6 address '%s'", scalar(node)); + set_str_if_null(cur_netdef->gateway6, scalar(node)); + g_warning("`gateway6` has been deprecated, use default routes instead.\n" + "See the 'Default routes' section of the documentation for more details."); + return TRUE; +} + +static gboolean +handle_wifi_access_points(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + + key = yaml_document_get_node(doc, entry->key); + assert_type(key, YAML_SCALAR_NODE); + value = yaml_document_get_node(doc, entry->value); + assert_type(value, YAML_MAPPING_NODE); + + g_assert(cur_access_point == NULL); + cur_access_point = g_new0(NetplanWifiAccessPoint, 1); + cur_access_point->ssid = g_strdup(scalar(key)); + g_debug("%s: adding wifi AP '%s'", cur_netdef->id, cur_access_point->ssid); + + if (!cur_netdef->access_points) + cur_netdef->access_points = g_hash_table_new(g_str_hash, g_str_equal); + if (!g_hash_table_insert(cur_netdef->access_points, cur_access_point->ssid, cur_access_point)) { + /* Even in the error case, NULL out cur_access_point. Otherwise we + * have an assert failure if we do a multi-pass parse. */ + gboolean ret; + + ret = yaml_error(key, error, "%s: Duplicate access point SSID '%s'", cur_netdef->id, cur_access_point->ssid); + cur_access_point = NULL; + return ret; + } + + if (!process_mapping(doc, value, wifi_access_point_handlers, NULL, error)) { + cur_access_point = NULL; + return FALSE; + } + + cur_access_point = NULL; + } + return TRUE; +} + +/** + * Handler for bridge "interfaces:" list. We don't store that list in cur_netdef, + * but set cur_netdef's ID in all listed interfaces' "bond" or "bridge" field. + * @data: ignored + */ +static gboolean +handle_bridge_interfaces(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + /* all entries must refer to already defined IDs */ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + NetplanNetDefinition *component; + + assert_type(entry, YAML_SCALAR_NODE); + component = g_hash_table_lookup(netdefs, scalar(entry)); + if (!component) { + add_missing_node(entry); + } else { + if (component->bridge && g_strcmp0(component->bridge, cur_netdef->id) != 0) + return yaml_error(node, error, "%s: interface '%s' is already assigned to bridge %s", + cur_netdef->id, scalar(entry), component->bridge); + if (component->bond) + return yaml_error(node, error, "%s: interface '%s' is already assigned to bond %s", + cur_netdef->id, scalar(entry), component->bond); + set_str_if_null(component->bridge, cur_netdef->id); + if (component->backend == NETPLAN_BACKEND_OVS) { + g_debug("%s: Bridge contains openvswitch interface, choosing OVS backend", cur_netdef->id); + cur_netdef->backend = NETPLAN_BACKEND_OVS; + } + } + } + + return TRUE; +} + +/** + * Handler for bond "mode" types. + * @data: offset into NetplanNetDefinition where the const char* field to write is + * located + */ +static gboolean +handle_bond_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (!(strcmp(scalar(node), "balance-rr") == 0 || + strcmp(scalar(node), "active-backup") == 0 || + strcmp(scalar(node), "balance-xor") == 0 || + strcmp(scalar(node), "broadcast") == 0 || + strcmp(scalar(node), "802.3ad") == 0 || + strcmp(scalar(node), "balance-tlb") == 0 || + strcmp(scalar(node), "balance-alb") == 0 || + strcmp(scalar(node), "balance-tcp") == 0 || // only supported for OVS + strcmp(scalar(node), "balance-slb") == 0)) // only supported for OVS + return yaml_error(node, error, "unknown bond mode '%s'", scalar(node)); + + /* Implicitly set NETPLAN_BACKEND_OVS if ovs-only mode selected */ + if (!strcmp(scalar(node), "balance-tcp") || + !strcmp(scalar(node), "balance-slb")) { + g_debug("%s: mode '%s' only supported with openvswitch, choosing this backend", + cur_netdef->id, scalar(node)); + cur_netdef->backend = NETPLAN_BACKEND_OVS; + } + + return handle_netdef_str(doc, node, data, error); +} + +/** + * Handler for bond "interfaces:" list. + * @data: ignored + */ +static gboolean +handle_bond_interfaces(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + /* all entries must refer to already defined IDs */ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + NetplanNetDefinition *component; + + assert_type(entry, YAML_SCALAR_NODE); + component = g_hash_table_lookup(netdefs, scalar(entry)); + if (!component) { + add_missing_node(entry); + } else { + if (component->bridge) + return yaml_error(node, error, "%s: interface '%s' is already assigned to bridge %s", + cur_netdef->id, scalar(entry), component->bridge); + if (component->bond && g_strcmp0(component->bond, cur_netdef->id) != 0) + return yaml_error(node, error, "%s: interface '%s' is already assigned to bond %s", + cur_netdef->id, scalar(entry), component->bond); + component->bond = g_strdup(cur_netdef->id); + if (component->backend == NETPLAN_BACKEND_OVS) { + g_debug("%s: Bond contains openvswitch interface, choosing OVS backend", cur_netdef->id); + cur_netdef->backend = NETPLAN_BACKEND_OVS; + } + } + } + + return TRUE; +} + + +static gboolean +handle_nameservers_search(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + if (!cur_netdef->search_domains) + cur_netdef->search_domains = g_array_new(FALSE, FALSE, sizeof(char*)); + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->search_domains, s); + } + return TRUE; +} + +static gboolean +handle_nameservers_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + + /* is it an IPv4 address? */ + if (is_ip4_address(scalar(entry))) { + if (!cur_netdef->ip4_nameservers) + cur_netdef->ip4_nameservers = g_array_new(FALSE, FALSE, sizeof(char*)); + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->ip4_nameservers, s); + continue; + } + + /* is it an IPv6 address? */ + if (is_ip6_address(scalar(entry))) { + if (!cur_netdef->ip6_nameservers) + cur_netdef->ip6_nameservers = g_array_new(FALSE, FALSE, sizeof(char*)); + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->ip6_nameservers, s); + continue; + } + + return yaml_error(node, error, "malformed address '%s', must be X.X.X.X or X:X:X:X:X:X:X:X", scalar(entry)); + } + + return TRUE; +} + +static gboolean +handle_link_local(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ipv4 = FALSE; + gboolean ipv6 = FALSE; + + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + + assert_type(entry, YAML_SCALAR_NODE); + + if (g_ascii_strcasecmp(scalar(entry), "ipv4") == 0) + ipv4 = TRUE; + else if (g_ascii_strcasecmp(scalar(entry), "ipv6") == 0) + ipv6 = TRUE; + else + return yaml_error(node, error, "invalid value for link-local: '%s'", scalar(entry)); + } + + cur_netdef->linklocal.ipv4 = ipv4; + cur_netdef->linklocal.ipv6 = ipv6; + + return TRUE; +} + +struct NetplanOptionalAddressType NETPLAN_OPTIONAL_ADDRESS_TYPES[] = { + {"ipv4-ll", NETPLAN_OPTIONAL_IPV4_LL}, + {"ipv6-ra", NETPLAN_OPTIONAL_IPV6_RA}, + {"dhcp4", NETPLAN_OPTIONAL_DHCP4}, + {"dhcp6", NETPLAN_OPTIONAL_DHCP6}, + {"static", NETPLAN_OPTIONAL_STATIC}, + {NULL}, +}; + +static gboolean +handle_optional_addresses(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + int found = FALSE; + + for (unsigned i = 0; NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name != NULL; ++i) { + if (g_ascii_strcasecmp(scalar(entry), NETPLAN_OPTIONAL_ADDRESS_TYPES[i].name) == 0) { + cur_netdef->optional_addresses |= NETPLAN_OPTIONAL_ADDRESS_TYPES[i].flag; + found = TRUE; + break; + } + } + if (!found) { + return yaml_error(node, error, "invalid value for optional-addresses: '%s'", scalar(entry)); + } + } + return TRUE; +} + +static int +get_ip_family(const char* address) +{ + g_autofree char *ip_str; + char *prefix_len; + + ip_str = g_strdup(address); + prefix_len = strrchr(ip_str, '/'); + if (prefix_len) + *prefix_len = '\0'; + + if (is_ip4_address(ip_str)) + return AF_INET; + + if (is_ip6_address(ip_str)) + return AF_INET6; + + return -1; +} + +static gboolean +check_and_set_family(int family, guint* dest) +{ + if (*dest != -1 && *dest != family) + return FALSE; + + *dest = family; + + return TRUE; +} + +/* TODO: (cyphermox) Refactor the functions below. There's a lot of room for reuse. */ + +static gboolean +handle_routes_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_route); + return handle_generic_bool(doc, node, cur_route, data, error); +} + +static gboolean +handle_routes_scope(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_route->scope) + g_free(cur_route->scope); + cur_route->scope = g_strdup(scalar(node)); + + if (g_ascii_strcasecmp(cur_route->scope, "global") == 0 || + g_ascii_strcasecmp(cur_route->scope, "link") == 0 || + g_ascii_strcasecmp(cur_route->scope, "host") == 0) + return TRUE; + + return yaml_error(node, error, "invalid route scope '%s'", cur_route->scope); +} + +static gboolean +handle_routes_type(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_route->type) + g_free(cur_route->type); + cur_route->type = g_strdup(scalar(node)); + + if (g_ascii_strcasecmp(cur_route->type, "unicast") == 0 || + g_ascii_strcasecmp(cur_route->type, "unreachable") == 0 || + g_ascii_strcasecmp(cur_route->type, "blackhole") == 0 || + g_ascii_strcasecmp(cur_route->type, "prohibit") == 0) + return TRUE; + + return yaml_error(node, error, "invalid route type '%s'", cur_route->type); +} + +static gboolean +handle_routes_ip(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + int family = get_ip_family(scalar(node)); + char** dest = (char**) ((void*) cur_route + offset); + + if (family < 0) + return yaml_error(node, error, "invalid IP family '%d'", family); + + if (!check_and_set_family(family, &cur_route->family)) + return yaml_error(node, error, "IP family mismatch in route to %s", scalar(node)); + + g_free(*dest); + *dest = g_strdup(scalar(node)); + + return TRUE; +} + +static gboolean +handle_routes_destination(yaml_document_t *doc, yaml_node_t *node, const void *data, GError **error) +{ + const char *addr = scalar(node); + if (g_strcmp0(addr, "default") != 0) + return handle_routes_ip(doc, node, route_offset(to), error); + set_str_if_null(cur_route->to, addr); + return TRUE; +} + +static gboolean +handle_ip_rule_ip(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + int family = get_ip_family(scalar(node)); + char** dest = (char**) ((void*) cur_ip_rule + offset); + + if (family < 0) + return yaml_error(node, error, "invalid IP family '%d'", family); + + if (!check_and_set_family(family, &cur_ip_rule->family)) + return yaml_error(node, error, "IP family mismatch in route to %s", scalar(node)); + + g_free(*dest); + *dest = g_strdup(scalar(node)); + + return TRUE; +} + +static gboolean +handle_ip_rule_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_ip_rule); + return handle_generic_guint(doc, node, cur_ip_rule, data, error); +} + +static gboolean +handle_routes_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_route); + return handle_generic_guint(doc, node, cur_route, data, error); +} + +static gboolean +handle_ip_rule_tos(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + gboolean ret = handle_generic_guint(doc, node, cur_ip_rule, data, error); + if (cur_ip_rule->tos > 255) + return yaml_error(node, error, "invalid ToS (must be between 0 and 255): %s", scalar(node)); + return ret; +} + +/**************************************************** + * Grammar and handlers for network config "bridge_params" entry + ****************************************************/ + +static gboolean +handle_bridge_path_cost(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + guint v; + gchar* endptr; + NetplanNetDefinition *component; + guint* ref_ptr; + + key = yaml_document_get_node(doc, entry->key); + assert_type(key, YAML_SCALAR_NODE); + value = yaml_document_get_node(doc, entry->value); + assert_type(value, YAML_SCALAR_NODE); + + component = g_hash_table_lookup(netdefs, scalar(key)); + if (!component) { + add_missing_node(key); + } else { + ref_ptr = ((guint*) ((void*) component + GPOINTER_TO_UINT(data))); + if (*ref_ptr) + return yaml_error(node, error, "%s: interface '%s' already has a path cost of %u", + cur_netdef->id, scalar(key), *ref_ptr); + + v = g_ascii_strtoull(scalar(value), &endptr, 10); + if (*endptr != '\0' || v > G_MAXUINT) + return yaml_error(node, error, "invalid unsigned int value '%s'", scalar(value)); + + g_debug("%s: adding path '%s' of cost: %d", cur_netdef->id, scalar(key), v); + + *ref_ptr = v; + } + } + return TRUE; +} + +static gboolean +handle_bridge_port_priority(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + guint v; + gchar* endptr; + NetplanNetDefinition *component; + guint* ref_ptr; + + key = yaml_document_get_node(doc, entry->key); + assert_type(key, YAML_SCALAR_NODE); + value = yaml_document_get_node(doc, entry->value); + assert_type(value, YAML_SCALAR_NODE); + + component = g_hash_table_lookup(netdefs, scalar(key)); + if (!component) { + add_missing_node(key); + } else { + ref_ptr = ((guint*) ((void*) component + GPOINTER_TO_UINT(data))); + if (*ref_ptr) + return yaml_error(node, error, "%s: interface '%s' already has a port priority of %u", + cur_netdef->id, scalar(key), *ref_ptr); + + v = g_ascii_strtoull(scalar(value), &endptr, 10); + if (*endptr != '\0' || v > 63) + return yaml_error(node, error, "invalid port priority value (must be between 0 and 63): %s", + scalar(value)); + + g_debug("%s: adding port '%s' of priority: %d", cur_netdef->id, scalar(key), v); + + *ref_ptr = v; + } + } + return TRUE; +} + +static const mapping_entry_handler bridge_params_handlers[] = { + {"ageing-time", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bridge_params.ageing_time)}, + {"forward-delay", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bridge_params.forward_delay)}, + {"hello-time", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bridge_params.hello_time)}, + {"max-age", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bridge_params.max_age)}, + {"path-cost", YAML_MAPPING_NODE, handle_bridge_path_cost, NULL, netdef_offset(bridge_params.path_cost)}, + {"port-priority", YAML_MAPPING_NODE, handle_bridge_port_priority, NULL, netdef_offset(bridge_params.port_priority)}, + {"priority", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bridge_params.priority)}, + {"stp", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(bridge_params.stp)}, + {NULL} +}; + +static gboolean +handle_bridge(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + cur_netdef->custom_bridging = TRUE; + cur_netdef->bridge_params.stp = TRUE; + return process_mapping(doc, node, bridge_params_handlers, NULL, error); +} + +/**************************************************** + * Grammar and handlers for network config "routes" entry + ****************************************************/ + +static const mapping_entry_handler routes_handlers[] = { + {"from", YAML_SCALAR_NODE, handle_routes_ip, NULL, route_offset(from)}, + {"on-link", YAML_SCALAR_NODE, handle_routes_bool, NULL, route_offset(onlink)}, + {"scope", YAML_SCALAR_NODE, handle_routes_scope}, + {"table", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(table)}, + {"to", YAML_SCALAR_NODE, handle_routes_destination}, + {"type", YAML_SCALAR_NODE, handle_routes_type}, + {"via", YAML_SCALAR_NODE, handle_routes_ip, NULL, route_offset(via)}, + {"metric", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(metric)}, + {"mtu", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(mtubytes)}, + {"congestion-window", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(congestion_window)}, + {"advertised-receive-window", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(advertised_receive_window)}, + {NULL} +}; + +static gboolean +handle_routes(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + if (!cur_netdef->routes) + cur_netdef->routes = g_array_new(FALSE, TRUE, sizeof(NetplanIPRoute*)); + + /* Avoid adding the same routes in a 2nd parsing pass by comparing + * the array size to the YAML sequence size. Skip if they are equal. */ + guint item_count = node->data.sequence.items.top - node->data.sequence.items.start; + if (cur_netdef->routes->len == item_count) { + g_debug("%s: all routes have already been added", cur_netdef->id); + return TRUE; + } + + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_MAPPING_NODE); + + g_assert(cur_route == NULL); + cur_route = g_new0(NetplanIPRoute, 1); + cur_route->type = g_strdup("unicast"); + cur_route->scope = g_strdup("global"); + cur_route->family = G_MAXUINT; /* 0 is a valid family ID */ + cur_route->metric = NETPLAN_METRIC_UNSPEC; /* 0 is a valid metric */ + cur_route->table = NETPLAN_ROUTE_TABLE_UNSPEC; + g_debug("%s: adding new route", cur_netdef->id); + + if (!process_mapping(doc, entry, routes_handlers, NULL, error)) + goto err; + + if ( ( g_ascii_strcasecmp(cur_route->scope, "link") == 0 + || g_ascii_strcasecmp(cur_route->scope, "host") == 0) + && !cur_route->to) { + yaml_error(node, error, "link and host routes must specify a 'to' IP"); + goto err; + } else if ( g_ascii_strcasecmp(cur_route->type, "unicast") == 0 + && g_ascii_strcasecmp(cur_route->scope, "global") == 0 + && (!cur_route->to || !cur_route->via)) { + yaml_error(node, error, "unicast route must include both a 'to' and 'via' IP"); + goto err; + } else if (g_ascii_strcasecmp(cur_route->type, "unicast") != 0 && !cur_route->to) { + yaml_error(node, error, "non-unicast routes must specify a 'to' IP"); + goto err; + } + + g_array_append_val(cur_netdef->routes, cur_route); + cur_route = NULL; + } + return TRUE; + +err: + if (cur_route) { + g_free(cur_route); + cur_route = NULL; + } + return FALSE; +} + +static const mapping_entry_handler ip_rules_handlers[] = { + {"from", YAML_SCALAR_NODE, handle_ip_rule_ip, NULL, ip_rule_offset(from)}, + {"mark", YAML_SCALAR_NODE, handle_ip_rule_guint, NULL, ip_rule_offset(fwmark)}, + {"priority", YAML_SCALAR_NODE, handle_ip_rule_guint, NULL, ip_rule_offset(priority)}, + {"table", YAML_SCALAR_NODE, handle_ip_rule_guint, NULL, ip_rule_offset(table)}, + {"to", YAML_SCALAR_NODE, handle_ip_rule_ip, NULL, ip_rule_offset(to)}, + {"type-of-service", YAML_SCALAR_NODE, handle_ip_rule_tos, NULL, ip_rule_offset(tos)}, + {NULL} +}; + +static gboolean +handle_ip_rules(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + + cur_ip_rule = g_new0(NetplanIPRule, 1); + cur_ip_rule->family = G_MAXUINT; /* 0 is a valid family ID */ + cur_ip_rule->priority = NETPLAN_IP_RULE_PRIO_UNSPEC; + cur_ip_rule->table = NETPLAN_ROUTE_TABLE_UNSPEC; + cur_ip_rule->tos = NETPLAN_IP_RULE_TOS_UNSPEC; + cur_ip_rule->fwmark = NETPLAN_IP_RULE_FW_MARK_UNSPEC; + + if (process_mapping(doc, entry, ip_rules_handlers, NULL, error)) { + if (!cur_netdef->ip_rules) { + cur_netdef->ip_rules = g_array_new(FALSE, FALSE, sizeof(NetplanIPRule*)); + } + + g_array_append_val(cur_netdef->ip_rules, cur_ip_rule); + } + + if (!cur_ip_rule->from && !cur_ip_rule->to) + return yaml_error(node, error, "IP routing policy must include either a 'from' or 'to' IP"); + + cur_ip_rule = NULL; + + if (error && *error) + return FALSE; + } + return TRUE; +} + +/**************************************************** + * Grammar and handlers for bond parameters + ****************************************************/ + +static gboolean +handle_arp_ip_targets(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + if (!cur_netdef->bond_params.arp_ip_targets) { + cur_netdef->bond_params.arp_ip_targets = g_array_new(FALSE, FALSE, sizeof(char *)); + } + + /* Avoid adding the same arp_ip_targets in a 2nd parsing pass by comparing + * the array size to the YAML sequence size. Skip if they are equal. */ + guint item_count = node->data.sequence.items.top - node->data.sequence.items.start; + if (cur_netdef->bond_params.arp_ip_targets->len == item_count) { + g_debug("%s: all arp ip targets have already been added", cur_netdef->id); + return TRUE; + } + + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + g_autofree char* addr = NULL; + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + + addr = g_strdup(scalar(entry)); + + /* is it an IPv4 address? */ + if (is_ip4_address(addr)) { + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->bond_params.arp_ip_targets, s); + continue; + } + + return yaml_error(node, error, "malformed address '%s', must be X.X.X.X or X:X:X:X:X:X:X:X", scalar(entry)); + } + + return TRUE; +} + +static gboolean +handle_bond_primary_slave(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + NetplanNetDefinition *component; + char** ref_ptr; + + component = g_hash_table_lookup(netdefs, scalar(node)); + if (!component) { + add_missing_node(node); + } else { + /* If this is not the primary pass, the primary slave might already be equally set. */ + if (!g_strcmp0(cur_netdef->bond_params.primary_slave, scalar(node))) { + return TRUE; + } else if (cur_netdef->bond_params.primary_slave) + return yaml_error(node, error, "%s: bond already has a primary slave: %s", + cur_netdef->id, cur_netdef->bond_params.primary_slave); + + ref_ptr = ((char**) ((void*) component + GPOINTER_TO_UINT(data))); + *ref_ptr = g_strdup(scalar(node)); + cur_netdef->bond_params.primary_slave = g_strdup(scalar(node)); + } + + return TRUE; +} + +static const mapping_entry_handler bond_params_handlers[] = { + {"mode", YAML_SCALAR_NODE, handle_bond_mode, NULL, netdef_offset(bond_params.mode)}, + {"lacp-rate", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.lacp_rate)}, + {"mii-monitor-interval", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.monitor_interval)}, + {"min-links", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.min_links)}, + {"transmit-hash-policy", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.transmit_hash_policy)}, + {"ad-select", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.selection_logic)}, + {"all-slaves-active", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(bond_params.all_slaves_active)}, + {"arp-interval", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.arp_interval)}, + /* TODO: arp_ip_targets */ + {"arp-ip-targets", YAML_SEQUENCE_NODE, handle_arp_ip_targets}, + {"arp-validate", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.arp_validate)}, + {"arp-all-targets", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.arp_all_targets)}, + {"up-delay", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.up_delay)}, + {"down-delay", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.down_delay)}, + {"fail-over-mac-policy", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.fail_over_mac_policy)}, + {"gratuitous-arp", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.gratuitous_arp)}, + /* Handle the old misspelling */ + {"gratuitious-arp", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.gratuitous_arp)}, + /* TODO: unsolicited_na */ + {"packets-per-slave", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.packets_per_slave)}, + {"primary-reselect-policy", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.primary_reselect_policy)}, + {"resend-igmp", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(bond_params.resend_igmp)}, + {"learn-packet-interval", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(bond_params.learn_interval)}, + {"primary", YAML_SCALAR_NODE, handle_bond_primary_slave, NULL, netdef_offset(bond_params.primary_slave)}, + {NULL} +}; + +static gboolean +handle_bonding(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + return process_mapping(doc, node, bond_params_handlers, NULL, error); +} + +static gboolean +handle_dhcp_identifier(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_netdef->dhcp_identifier) + g_free(cur_netdef->dhcp_identifier); + cur_netdef->dhcp_identifier = g_strdup(scalar(node)); + + if (g_ascii_strcasecmp(cur_netdef->dhcp_identifier, "duid") == 0 || + g_ascii_strcasecmp(cur_netdef->dhcp_identifier, "mac") == 0) + return TRUE; + + return yaml_error(node, error, "invalid DHCP client identifier type '%s'", cur_netdef->dhcp_identifier); +} + +/**************************************************** + * Grammar and handlers for tunnels + ****************************************************/ + +const char* +tunnel_mode_to_string(NetplanTunnelMode mode) +{ + return netplan_tunnel_mode_table[mode]; +} + +static gboolean +handle_tunnel_addr(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_autofree char* addr = NULL; + char* prefix_len; + + /* split off /prefix_len */ + addr = g_strdup(scalar(node)); + prefix_len = strrchr(addr, '/'); + if (prefix_len) + return yaml_error(node, error, "address '%s' should not include /prefixlength", scalar(node)); + + /* is it an IPv4 address? */ + if (is_ip4_address(addr)) + return handle_netdef_ip4(doc, node, data, error); + + /* is it an IPv6 address? */ + if (is_ip6_address(addr)) + return handle_netdef_ip6(doc, node, data, error); + + return yaml_error(node, error, "malformed address '%s', must be X.X.X.X or X:X:X:X:X:X:X:X", scalar(node)); +} + +static gboolean +handle_tunnel_mode(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + const char *key = scalar(node); + NetplanTunnelMode i; + + // Skip over unknown (0) tunnel mode. + for (i = 1; i < NETPLAN_TUNNEL_MODE_MAX_; ++i) { + if (g_strcmp0(netplan_tunnel_mode_table[i], key) == 0) { + cur_netdef->tunnel.mode = i; + return TRUE; + } + } + + return yaml_error(node, error, "%s: tunnel mode '%s' is not supported", cur_netdef->id, key); +} + +static const mapping_entry_handler tunnel_keys_handlers[] = { + {"input", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(tunnel.input_key)}, + {"output", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(tunnel.output_key)}, + {"private", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(tunnel.private_key)}, + {NULL} +}; + +static gboolean +handle_tunnel_key_mapping(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ret = FALSE; + + /* We overload the 'key[s]' setting for tunnels; such that it can either be a + * single scalar with the same key to use for both input, output and private + * keys, or a mapping where one can specify each. */ + if (node->type == YAML_SCALAR_NODE) { + ret = handle_netdef_str(doc, node, netdef_offset(tunnel.input_key), error); + if (ret) + ret = handle_netdef_str(doc, node, netdef_offset(tunnel.output_key), error); + if (ret) + ret = handle_netdef_str(doc, node, netdef_offset(tunnel.private_key), error); + } else if (node->type == YAML_MAPPING_NODE) + ret = process_mapping(doc, node, tunnel_keys_handlers, NULL, error); + else + return yaml_error(node, error, "invalid type for 'key[s]': must be a scalar or mapping"); + + return ret; +} + +/** + * Handler for setting a NetplanWireguardPeer string field from a scalar node + * @data: pointer to the const char* field to write + */ +static gboolean +handle_wireguard_peer_str(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_wireguard_peer); + return handle_generic_str(doc, node, cur_wireguard_peer, data, error); +} + +/** + * Handler for setting a NetplanWireguardPeer string field from a scalar node + * @data: pointer to the guint field to write + */ +static gboolean +handle_wireguard_peer_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_wireguard_peer); + return handle_generic_guint(doc, node, cur_wireguard_peer, data, error); +} + +static gboolean +handle_wireguard_allowed_ips(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + return handle_generic_addresses(doc, node, FALSE, &(cur_wireguard_peer->allowed_ips), + &(cur_wireguard_peer->allowed_ips), error); +} + +static gboolean +handle_wireguard_endpoint(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + g_autofree char* endpoint = NULL; + char* port; + char* address; + guint64 port_num; + + endpoint = g_strdup(scalar(node)); + /* absolute minimal length of endpoint is 3 chars: 'h:8' */ + if (strlen(endpoint) < 3) { + return yaml_error(node, error, "invalid endpoint address or hostname '%s'", scalar(node)); + } + if (endpoint[0] == '[') { + /* this is an ipv6 endpoint in [ad:rr:ee::ss]:port form */ + char *endbrace = strrchr(endpoint, ']'); + if (!endbrace) + return yaml_error(node, error, "invalid address in endpoint '%s'", scalar(node)); + address = endpoint + 1; + *endbrace = '\0'; + port = strrchr(endbrace + 1, ':'); + } else { + address = endpoint; + port = strrchr(endpoint, ':'); + } + /* split off :port */ + if (!port) + return yaml_error(node, error, "endpoint '%s' is missing :port", scalar(node)); + *port = '\0'; + port++; /* skip former ':' into first char of port */ + port_num = g_ascii_strtoull(port, NULL, 10); + if (port_num > 65535) + return yaml_error(node, error, "invalid port in endpoint '%s'", scalar(node)); + if (is_ip4_address(address) || is_ip6_address(address) || is_hostname(address)) { + return handle_wireguard_peer_str(doc, node, wireguard_peer_offset(endpoint), error); + } + return yaml_error(node, error, "invalid endpoint address or hostname '%s'", scalar(node)); +} + +static const mapping_entry_handler wireguard_peer_keys_handlers[] = { + {"public", YAML_SCALAR_NODE, handle_wireguard_peer_str, NULL, wireguard_peer_offset(public_key)}, + {"shared", YAML_SCALAR_NODE, handle_wireguard_peer_str, NULL, wireguard_peer_offset(preshared_key)}, + {NULL} +}; + +static gboolean +handle_wireguard_peer_key_mapping(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + return process_mapping(doc, node, wireguard_peer_keys_handlers, NULL, error); +} + +const mapping_entry_handler wireguard_peer_handlers[] = { + {"keys", YAML_MAPPING_NODE, handle_wireguard_peer_key_mapping}, + {"keepalive", YAML_SCALAR_NODE, handle_wireguard_peer_guint, NULL, wireguard_peer_offset(keepalive)}, + {"endpoint", YAML_SCALAR_NODE, handle_wireguard_endpoint}, + {"allowed-ips", YAML_SEQUENCE_NODE, handle_wireguard_allowed_ips}, + {NULL} +}; + +static gboolean +handle_wireguard_peers(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + if (!cur_netdef->wireguard_peers) + cur_netdef->wireguard_peers = g_array_new(FALSE, TRUE, sizeof(NetplanWireguardPeer*)); + + /* Avoid adding the same peers in a 2nd parsing pass by comparing + * the array size to the YAML sequence size. Skip if they are equal. */ + guint item_count = node->data.sequence.items.top - node->data.sequence.items.start; + if (cur_netdef->wireguard_peers->len == item_count) { + g_debug("%s: all wireguard peers have already been added", cur_netdef->id); + return TRUE; + } + + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_MAPPING_NODE); + + g_assert(cur_wireguard_peer == NULL); + cur_wireguard_peer = g_new0(NetplanWireguardPeer, 1); + cur_wireguard_peer->allowed_ips = g_array_new(FALSE, FALSE, sizeof(char*)); + g_debug("%s: adding new wireguard peer", cur_netdef->id); + + g_array_append_val(cur_netdef->wireguard_peers, cur_wireguard_peer); + if (!process_mapping(doc, entry, wireguard_peer_handlers, NULL, error)) { + cur_wireguard_peer = NULL; + return FALSE; + } + cur_wireguard_peer = NULL; + } + return TRUE; +} + +/**************************************************** + * Grammar and handlers for network devices + ****************************************************/ + +static gboolean +handle_ovs_bond_lacp(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_netdef->type != NETPLAN_DEF_TYPE_BOND) + return yaml_error(node, error, "Key 'lacp' is only valid for interface type 'openvswitch bond'"); + + if (g_strcmp0(scalar(node), "active") && g_strcmp0(scalar(node), "passive") && g_strcmp0(scalar(node), "off")) + return yaml_error(node, error, "Value of 'lacp' needs to be 'active', 'passive' or 'off"); + + return handle_netdef_str(doc, node, data, error); +} + +static gboolean +handle_ovs_bridge_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) + return yaml_error(node, error, "Key is only valid for interface type 'openvswitch bridge'"); + + return handle_netdef_bool(doc, node, data, error); +} + +static gboolean +handle_ovs_bridge_fail_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) + return yaml_error(node, error, "Key 'fail-mode' is only valid for interface type 'openvswitch bridge'"); + + if (g_strcmp0(scalar(node), "standalone") && g_strcmp0(scalar(node), "secure")) + return yaml_error(node, error, "Value of 'fail-mode' needs to be 'standalone' or 'secure'"); + + return handle_netdef_str(doc, node, data, error); +} + +static gboolean +handle_ovs_protocol(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + const char* supported[] = { + "OpenFlow10", "OpenFlow11", "OpenFlow12", "OpenFlow13", "OpenFlow14", "OpenFlow15", "OpenFlow16", NULL + }; + unsigned i = 0; + guint offset = GPOINTER_TO_UINT(data); + GArray** protocols = (GArray**) ((void*) entryptr + offset); + + for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) { + yaml_node_t *entry = yaml_document_get_node(doc, *iter); + assert_type(entry, YAML_SCALAR_NODE); + + for (i = 0; supported[i] != NULL; ++i) + if (!g_strcmp0(scalar(entry), supported[i])) + break; + + if (supported[i] == NULL) + return yaml_error(node, error, "Unsupported OVS 'protocol' value: %s", scalar(entry)); + + if (!*protocols) + *protocols = g_array_new(FALSE, FALSE, sizeof(char*)); + char* s = g_strdup(scalar(entry)); + g_array_append_val(*protocols, s); + } + + return TRUE; +} + +static gboolean +handle_ovs_bridge_protocol(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) + return yaml_error(node, error, "Key 'protocols' is only valid for interface type 'openvswitch bridge'"); + + return handle_ovs_protocol(doc, node, cur_netdef, data, error); +} + +static gboolean +handle_ovs_bridge_controller_connection_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) + return yaml_error(node, error, "Key 'controller.connection-mode' is only valid for interface type 'openvswitch bridge'"); + + if (g_strcmp0(scalar(node), "in-band") && g_strcmp0(scalar(node), "out-of-band")) + return yaml_error(node, error, "Value of 'connection-mode' needs to be 'in-band' or 'out-of-band'"); + + return handle_netdef_str(doc, node, data, error); +} + +static gboolean +handle_ovs_bridge_controller_addresses(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) + return yaml_error(node, error, "Key 'controller.addresses' is only valid for interface type 'openvswitch bridge'"); + + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { + gchar** vec = NULL; + gboolean is_host = FALSE; + gboolean is_port = FALSE; + gboolean is_unix = FALSE; + + yaml_node_t *entry = yaml_document_get_node(doc, *i); + assert_type(entry, YAML_SCALAR_NODE); + /* We always need at least one colon */ + if (!g_strrstr(scalar(entry), ":")) + return yaml_error(node, error, "Unsupported OVS controller target: %s", scalar(entry)); + + vec = g_strsplit (scalar(entry), ":", 2); + + is_host = !g_strcmp0(vec[0], "tcp") || !g_strcmp0(vec[0], "ssl"); + is_port = !g_strcmp0(vec[0], "ptcp") || !g_strcmp0(vec[0], "pssl"); + is_unix = !g_strcmp0(vec[0], "unix") || !g_strcmp0(vec[0], "punix"); + + if (!cur_netdef->ovs_settings.controller.addresses) + cur_netdef->ovs_settings.controller.addresses = g_array_new(FALSE, FALSE, sizeof(char*)); + + /* Format: [p]unix:file */ + if (is_unix && vec[1] != NULL && vec[2] == NULL) { + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->ovs_settings.controller.addresses, s); + g_strfreev(vec); + continue; + /* Format tcp:host[:port] or ssl:host[:port] */ + } else if (is_host && validate_ovs_target(TRUE, vec[1])) { + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->ovs_settings.controller.addresses, s); + g_strfreev(vec); + continue; + /* Format ptcp:[port][:host] or pssl:[port][:host] */ + } else if (is_port && validate_ovs_target(FALSE, vec[1])) { + char* s = g_strdup(scalar(entry)); + g_array_append_val(cur_netdef->ovs_settings.controller.addresses, s); + g_strfreev(vec); + continue; + } + + g_strfreev(vec); + return yaml_error(node, error, "Unsupported OVS controller target: %s", scalar(entry)); + } + + return TRUE; +} + +static const mapping_entry_handler ovs_controller_handlers[] = { + {"addresses", YAML_SEQUENCE_NODE, handle_ovs_bridge_controller_addresses, NULL, netdef_offset(ovs_settings.controller.addresses)}, + {"connection-mode", YAML_SCALAR_NODE, handle_ovs_bridge_controller_connection_mode, NULL, netdef_offset(ovs_settings.controller.connection_mode)}, + {NULL}, +}; + +static const mapping_entry_handler ovs_backend_settings_handlers[] = { + {"external-ids", YAML_MAPPING_NODE, handle_netdef_map, NULL, netdef_offset(ovs_settings.external_ids)}, + {"other-config", YAML_MAPPING_NODE, handle_netdef_map, NULL, netdef_offset(ovs_settings.other_config)}, + {"lacp", YAML_SCALAR_NODE, handle_ovs_bond_lacp, NULL, netdef_offset(ovs_settings.lacp)}, + {"fail-mode", YAML_SCALAR_NODE, handle_ovs_bridge_fail_mode, NULL, netdef_offset(ovs_settings.fail_mode)}, + {"mcast-snooping", YAML_SCALAR_NODE, handle_ovs_bridge_bool, NULL, netdef_offset(ovs_settings.mcast_snooping)}, + {"rstp", YAML_SCALAR_NODE, handle_ovs_bridge_bool, NULL, netdef_offset(ovs_settings.rstp)}, + {"protocols", YAML_SEQUENCE_NODE, handle_ovs_bridge_protocol, NULL, netdef_offset(ovs_settings.protocols)}, + {"controller", YAML_MAPPING_NODE, NULL, ovs_controller_handlers}, + {NULL} +}; + +static gboolean +handle_ovs_backend(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + GList* values = NULL; + gboolean ret = process_mapping(doc, node, ovs_backend_settings_handlers, &values, error); + guint len = g_list_length(values); + + if (cur_netdef->type != NETPLAN_DEF_TYPE_BOND && cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) { + GList *other_config = g_list_find_custom(values, "other-config", (GCompareFunc) strcmp); + GList *external_ids = g_list_find_custom(values, "external-ids", (GCompareFunc) strcmp); + /* Non-bond/non-bridge interfaces might still be handled by the networkd backend */ + if (len == 1 && (other_config || external_ids)) + return ret; + else if (len == 2 && other_config && external_ids) + return ret; + } + g_list_free_full(values, g_free); + + /* Set the renderer for this device to NETPLAN_BACKEND_OVS, implicitly. + * But only if empty "openvswitch: {}" or "openvswitch:" with more than + * "other-config" or "external-ids" keys is given. */ + cur_netdef->backend = NETPLAN_BACKEND_OVS; + return ret; +} + +static const mapping_entry_handler nameservers_handlers[] = { + {"search", YAML_SEQUENCE_NODE, handle_nameservers_search}, + {"addresses", YAML_SEQUENCE_NODE, handle_nameservers_addresses}, + {NULL} +}; + +/* Handlers for DHCP overrides. */ +#define COMMON_DHCP_OVERRIDES_HANDLERS(overrides) \ + {"hostname", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(overrides.hostname)}, \ + {"route-metric", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(overrides.metric)}, \ + {"send-hostname", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.send_hostname)}, \ + {"use-dns", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_dns)}, \ + {"use-domains", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(overrides.use_domains)}, \ + {"use-hostname", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_hostname)}, \ + {"use-mtu", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_mtu)}, \ + {"use-ntp", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_ntp)}, \ + {"use-routes", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(overrides.use_routes)} + +static const mapping_entry_handler dhcp4_overrides_handlers[] = { + COMMON_DHCP_OVERRIDES_HANDLERS(dhcp4_overrides), + {NULL}, +}; + +static const mapping_entry_handler dhcp6_overrides_handlers[] = { + COMMON_DHCP_OVERRIDES_HANDLERS(dhcp6_overrides), + {NULL}, +}; + +/* Handlers shared by all link types */ +#define COMMON_LINK_HANDLERS \ + {"accept-ra", YAML_SCALAR_NODE, handle_accept_ra, NULL, netdef_offset(accept_ra)}, \ + {"activation-mode", YAML_SCALAR_NODE, handle_activation_mode, NULL, netdef_offset(activation_mode)}, \ + {"addresses", YAML_SEQUENCE_NODE, handle_addresses}, \ + {"critical", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(critical)}, \ + {"dhcp4", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp4)}, \ + {"dhcp6", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(dhcp6)}, \ + {"dhcp-identifier", YAML_SCALAR_NODE, handle_dhcp_identifier}, \ + {"dhcp4-overrides", YAML_MAPPING_NODE, NULL, dhcp4_overrides_handlers}, \ + {"dhcp6-overrides", YAML_MAPPING_NODE, NULL, dhcp6_overrides_handlers}, \ + {"gateway4", YAML_SCALAR_NODE, handle_gateway4}, \ + {"gateway6", YAML_SCALAR_NODE, handle_gateway6}, \ + {"ipv6-address-generation", YAML_SCALAR_NODE, handle_netdef_addrgen}, \ + {"ipv6-address-token", YAML_SCALAR_NODE, handle_netdef_addrtok, NULL, netdef_offset(ip6_addr_gen_token)}, \ + {"ipv6-mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(ipv6_mtubytes)}, \ + {"ipv6-privacy", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(ip6_privacy)}, \ + {"link-local", YAML_SEQUENCE_NODE, handle_link_local}, \ + {"macaddress", YAML_SCALAR_NODE, handle_netdef_mac, NULL, netdef_offset(set_mac)}, \ + {"mtu", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(mtubytes)}, \ + {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers}, \ + {"optional", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(optional)}, \ + {"optional-addresses", YAML_SEQUENCE_NODE, handle_optional_addresses}, \ + {"renderer", YAML_SCALAR_NODE, handle_netdef_renderer}, \ + {"routes", YAML_SEQUENCE_NODE, handle_routes}, \ + {"routing-policy", YAML_SEQUENCE_NODE, handle_ip_rules} + +#define COMMON_BACKEND_HANDLERS \ + {"networkmanager", YAML_MAPPING_NODE, NULL, nm_backend_settings_handlers}, \ + {"openvswitch", YAML_MAPPING_NODE, handle_ovs_backend} + +/* Handlers for physical links */ +#define PHYSICAL_LINK_HANDLERS \ + {"match", YAML_MAPPING_NODE, handle_match}, \ + {"set-name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(set_name)}, \ + {"wakeonlan", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(wake_on_lan)}, \ + {"wakeonwlan", YAML_SEQUENCE_NODE, handle_wowlan, NULL, netdef_offset(wowlan)}, \ + {"emit-lldp", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(emit_lldp)} + +static const mapping_entry_handler ethernet_def_handlers[] = { + COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, + PHYSICAL_LINK_HANDLERS, + {"auth", YAML_MAPPING_NODE, handle_auth}, + {"link", YAML_SCALAR_NODE, handle_netdef_id_ref, NULL, netdef_offset(sriov_link)}, + {"virtual-function-count", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(sriov_explicit_vf_count)}, + {NULL} +}; + +static const mapping_entry_handler wifi_def_handlers[] = { + COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, + PHYSICAL_LINK_HANDLERS, + {"access-points", YAML_MAPPING_NODE, handle_wifi_access_points}, + {"auth", YAML_MAPPING_NODE, handle_auth}, + {NULL} +}; + +static const mapping_entry_handler bridge_def_handlers[] = { + COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, + {"interfaces", YAML_SEQUENCE_NODE, handle_bridge_interfaces, NULL, NULL}, + {"parameters", YAML_MAPPING_NODE, handle_bridge}, + {NULL} +}; + +static const mapping_entry_handler bond_def_handlers[] = { + COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, + {"interfaces", YAML_SEQUENCE_NODE, handle_bond_interfaces, NULL, NULL}, + {"parameters", YAML_MAPPING_NODE, handle_bonding}, + {NULL} +}; + +static const mapping_entry_handler vlan_def_handlers[] = { + COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, + {"id", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(vlan_id)}, + {"link", YAML_SCALAR_NODE, handle_netdef_id_ref, NULL, netdef_offset(vlan_link)}, + {NULL} +}; + +static const mapping_entry_handler modem_def_handlers[] = { + COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, + PHYSICAL_LINK_HANDLERS, + {"apn", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.apn)}, + {"auto-config", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(modem_params.auto_config)}, + {"device-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.device_id)}, + {"network-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.network_id)}, + {"number", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.number)}, + {"password", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.password)}, + {"pin", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.pin)}, + {"sim-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.sim_id)}, + {"sim-operator-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.sim_operator_id)}, + {"username", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.username)}, +}; + +static const mapping_entry_handler tunnel_def_handlers[] = { + COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, + {"mode", YAML_SCALAR_NODE, handle_tunnel_mode}, + {"local", YAML_SCALAR_NODE, handle_tunnel_addr, NULL, netdef_offset(tunnel.local_ip)}, + {"remote", YAML_SCALAR_NODE, handle_tunnel_addr, NULL, netdef_offset(tunnel.remote_ip)}, + {"ttl", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(tunnel_ttl)}, + + /* Handle key/keys for clarity in config: this can be either a scalar or + * mapping of multiple keys (input and output) + */ + {"key", YAML_NO_NODE, handle_tunnel_key_mapping}, + {"keys", YAML_NO_NODE, handle_tunnel_key_mapping}, + + /* wireguard */ + {"mark", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(tunnel.fwmark)}, + {"port", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(tunnel.port)}, + {"peers", YAML_SEQUENCE_NODE, handle_wireguard_peers}, + {NULL} +}; + +/**************************************************** + * Grammar and handlers for network node + ****************************************************/ + +static gboolean +handle_network_version(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + long mangled_version; + + mangled_version = strtol(scalar(node), NULL, 10); + + if (mangled_version < NETPLAN_VERSION_MIN || mangled_version >= NETPLAN_VERSION_MAX) + return yaml_error(node, error, "Only version 2 is supported"); + return TRUE; +} + +static gboolean +handle_network_renderer(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + return parse_renderer(node, &backend_global, error); +} + +static gboolean +handle_network_ovs_settings_global(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_map(doc, node, &ovs_settings_global, data, error); +} + +static gboolean +handle_network_ovs_settings_global_protocol(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_ovs_protocol(doc, node, &ovs_settings_global, data, error); +} + +static gboolean +handle_network_ovs_settings_global_ports(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + yaml_node_t* port = NULL; + yaml_node_t* peer = NULL; + yaml_node_t* pair = NULL; + yaml_node_item_t *item = NULL; + NetplanNetDefinition *component = NULL; + + for (yaml_node_item_t *iter = node->data.sequence.items.start; iter < node->data.sequence.items.top; iter++) { + pair = yaml_document_get_node(doc, *iter); + assert_type(pair, YAML_SEQUENCE_NODE); + + item = pair->data.sequence.items.start; + /* A peer port definition must contain exactly 2 ports */ + if (item+2 != pair->data.sequence.items.top) { + return yaml_error(pair, error, "An openvswitch peer port sequence must have exactly two entries"); + } + + port = yaml_document_get_node(doc, *item); + assert_type(port, YAML_SCALAR_NODE); + peer = yaml_document_get_node(doc, *(item+1)); + assert_type(peer, YAML_SCALAR_NODE); + + /* Create port 1 netdef */ + component = netdefs ? g_hash_table_lookup(netdefs, scalar(port)) : NULL; + if (!component) { + component = netplan_netdef_new(scalar(port), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS); + if (g_hash_table_remove(missing_id, scalar(port))) + missing_ids_found++; + } + + if (component->peer && g_strcmp0(component->peer, scalar(peer))) + return yaml_error(port, error, "openvswitch port '%s' is already assigned to peer '%s'", + component->id, component->peer); + component->peer = g_strdup(scalar(peer)); + + /* Create port 2 (peer) netdef */ + component = NULL; + component = netdefs ? g_hash_table_lookup(netdefs, scalar(peer)) : NULL; + if (!component) { + component = netplan_netdef_new(scalar(peer), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS); + if (g_hash_table_remove(missing_id, scalar(peer))) + missing_ids_found++; + } + + if (component->peer && g_strcmp0(component->peer, scalar(port))) + return yaml_error(peer, error, "openvswitch port '%s' is already assigned to peer '%s'", + component->id, component->peer); + component->peer = g_strdup(scalar(port)); + } + return TRUE; +} + +/** + * Callback for a net device type entry like "ethernets:" in "network:" + * @data: netdef_type (as pointer) + */ +static gboolean +handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + const mapping_entry_handler* handlers; + + key = yaml_document_get_node(doc, entry->key); + if (!assert_valid_id(key, error)) + return FALSE; + /* globbing is not allowed for IDs */ + if (strpbrk(scalar(key), "*[]?")) + return yaml_error(key, error, "Definition ID '%s' must not use globbing", scalar(key)); + + value = yaml_document_get_node(doc, entry->value); + + /* special-case "renderer:" key to set the per-type backend */ + if (strcmp(scalar(key), "renderer") == 0) { + if (!parse_renderer(value, &backend_cur_type, error)) + return FALSE; + continue; + } + + assert_type(value, YAML_MAPPING_NODE); + + /* At this point we've seen a new starting definition, if it has been + * already mentioned in another netdef, removing it from our "missing" + * list. */ + if(g_hash_table_remove(missing_id, scalar(key))) + missing_ids_found++; + + cur_netdef = netdefs ? g_hash_table_lookup(netdefs, scalar(key)) : NULL; + if (cur_netdef) { + /* already exists, overriding/amending previous definition */ + if (cur_netdef->type != GPOINTER_TO_UINT(data)) + return yaml_error(key, error, "Updated definition '%s' changes device type", scalar(key)); + } else { + cur_netdef = netplan_netdef_new(scalar(key), GPOINTER_TO_UINT(data), backend_cur_type); + } + g_assert(cur_filename); + cur_netdef->filename = g_strdup(cur_filename); + + // XXX: breaks multi-pass parsing. + //if (!g_hash_table_add(ids_in_file, cur_netdef->id)) + // return yaml_error(key, error, "Duplicate net definition ID '%s'", cur_netdef->id); + + /* and fill it with definitions */ + switch (cur_netdef->type) { + case NETPLAN_DEF_TYPE_BOND: handlers = bond_def_handlers; break; + case NETPLAN_DEF_TYPE_BRIDGE: handlers = bridge_def_handlers; break; + case NETPLAN_DEF_TYPE_ETHERNET: handlers = ethernet_def_handlers; break; + case NETPLAN_DEF_TYPE_MODEM: handlers = modem_def_handlers; break; + case NETPLAN_DEF_TYPE_TUNNEL: handlers = tunnel_def_handlers; break; + case NETPLAN_DEF_TYPE_VLAN: handlers = vlan_def_handlers; break; + case NETPLAN_DEF_TYPE_WIFI: handlers = wifi_def_handlers; break; + case NETPLAN_DEF_TYPE_NM: + g_warning("netplan: %s: handling NetworkManager passthrough device, settings are not fully supported.", cur_netdef->id); + handlers = ethernet_def_handlers; + break; + default: g_assert_not_reached(); // LCOV_EXCL_LINE + } + if (!process_mapping(doc, value, handlers, NULL, error)) + return FALSE; + + /* validate definition-level conditions */ + if (!validate_netdef_grammar(cur_netdef, value, error)) + return FALSE; + + /* convenience shortcut: physical device without match: means match + * name on ID */ + if (cur_netdef->type < NETPLAN_DEF_TYPE_VIRTUAL && !cur_netdef->has_match) + set_str_if_null(cur_netdef->match.original_name, cur_netdef->id); + } + backend_cur_type = NETPLAN_BACKEND_NONE; + return TRUE; +} + +static const mapping_entry_handler ovs_global_ssl_handlers[] = { + {"ca-cert", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(ca_certificate)}, + {"certificate", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_certificate)}, + {"private-key", YAML_SCALAR_NODE, handle_auth_str, NULL, auth_offset(client_key)}, + {NULL} +}; + +static gboolean +handle_ovs_global_ssl(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) +{ + gboolean ret; + + cur_auth = &(ovs_settings_global.ssl); + ret = process_mapping(doc, node, ovs_global_ssl_handlers, NULL, error); + cur_auth = NULL; + + return ret; +} + +static const mapping_entry_handler ovs_network_settings_handlers[] = { + {"external-ids", YAML_MAPPING_NODE, handle_network_ovs_settings_global, NULL, ovs_settings_offset(external_ids)}, + {"other-config", YAML_MAPPING_NODE, handle_network_ovs_settings_global, NULL, ovs_settings_offset(other_config)}, + {"protocols", YAML_SEQUENCE_NODE, handle_network_ovs_settings_global_protocol, NULL, ovs_settings_offset(protocols)}, + {"ports", YAML_SEQUENCE_NODE, handle_network_ovs_settings_global_ports}, + {"ssl", YAML_MAPPING_NODE, handle_ovs_global_ssl}, + {NULL} +}; + +static const mapping_entry_handler network_handlers[] = { + {"bonds", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_BOND)}, + {"bridges", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_BRIDGE)}, + {"ethernets", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_ETHERNET)}, + {"renderer", YAML_SCALAR_NODE, handle_network_renderer}, + {"tunnels", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_TUNNEL)}, + {"version", YAML_SCALAR_NODE, handle_network_version}, + {"vlans", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_VLAN)}, + {"wifis", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_WIFI)}, + {"modems", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_MODEM)}, + {"nm-devices", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_NM)}, + {"openvswitch", YAML_MAPPING_NODE, NULL, ovs_network_settings_handlers}, + {NULL} +}; + +/**************************************************** + * Grammar and handlers for root node + ****************************************************/ + +static const mapping_entry_handler root_handlers[] = { + {"network", YAML_MAPPING_NODE, NULL, network_handlers}, + {NULL} +}; + +/** + * Handle multiple-pass parsing of the yaml document. + */ +static gboolean +process_document(yaml_document_t* doc, GError** error) +{ + gboolean ret; + int previously_found; + int still_missing; + + g_assert(missing_id == NULL); + missing_id = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free); + + do { + g_debug("starting new processing pass"); + + previously_found = missing_ids_found; + missing_ids_found = 0; + + g_clear_error(error); + + ret = process_mapping(doc, yaml_document_get_root_node(doc), root_handlers, NULL, error); + + still_missing = g_hash_table_size(missing_id); + + if (still_missing > 0 && missing_ids_found == previously_found) + break; + } while (still_missing > 0 || missing_ids_found > 0); + + if (g_hash_table_size(missing_id) > 0) { + GHashTableIter iter; + gpointer key, value; + NetplanMissingNode *missing; + + g_clear_error(error); + + /* Get the first missing identifier we can get from our list, to + * approximate early failure and give the user a meaningful error. */ + g_hash_table_iter_init (&iter, missing_id); + g_hash_table_iter_next (&iter, &key, &value); + missing = (NetplanMissingNode*) value; + + return yaml_error(missing->node, error, "%s: interface '%s' is not defined", + missing->netdef_id, + key); + } + + g_hash_table_destroy(missing_id); + missing_id = NULL; + return ret; +} + +/** + * Parse given YAML file and create/update global "netdefs" list. + */ +gboolean +netplan_parse_yaml(const char* filename, GError** error) +{ + yaml_document_t doc; + gboolean ret; + + if (!load_yaml(filename, &doc, error)) + return FALSE; + + /* empty file? */ + if (yaml_document_get_root_node(&doc) == NULL) + return TRUE; + + g_assert(ids_in_file == NULL); + ids_in_file = g_hash_table_new(g_str_hash, NULL); + + cur_filename = filename; + ret = process_document(&doc, error); + + cur_filename = NULL; + cur_netdef = NULL; + yaml_document_delete(&doc); + g_hash_table_destroy(ids_in_file); + ids_in_file = NULL; + return ret; +} + +static void +finish_iterator(gpointer key, gpointer value, gpointer user_data) +{ + GError **error = (GError **)user_data; + NetplanNetDefinition* nd = value; + + /* Take more steps to make sure we always have a backend set for netdefs */ + if (nd->backend == NETPLAN_BACKEND_NONE) { + nd->backend = get_default_backend_for_type(nd->type); + g_debug("%s: setting default backend to %i", nd->id, nd->backend); + } + + /* Do a final pass of validation for backend-specific conditions */ + if (validate_backend_rules(nd, error)) + g_debug("Configuration is valid"); +} + +/** + * Post-processing after parsing all config files + */ +GHashTable * +netplan_finish_parse(GError** error) +{ + if (netdefs) { + GError *recoverable = NULL; + g_debug("We have some netdefs, pass them through a final round of validation"); + if (!validate_default_route_consistency(netdefs, &recoverable)) { + g_warning("Problem encountered while validating default route consistency." + "Please set up multiple routing tables and use `routing-policy` instead.\n" + "Error: %s", (recoverable) ? recoverable->message : ""); + g_clear_error(&recoverable); + } + g_hash_table_foreach(netdefs, finish_iterator, error); + } + + if (error && *error) + return NULL; + + return netdefs; +} + +/** + * Return current global backend. + */ +NetplanBackend +netplan_get_global_backend() +{ + return backend_global; +} + +/** + * Clear NetplanNetDefinition hashtable + */ +guint +netplan_clear_netdefs() +{ + guint n = 0; + if(netdefs) { + n = g_hash_table_size(netdefs); + /* FIXME: make sure that any dynamically allocated netdef data is freed */ + if (n > 0) + g_hash_table_remove_all(netdefs); + netdefs = NULL; + } + if(netdefs_ordered) { + g_clear_list(&netdefs_ordered, g_free); + netdefs_ordered = NULL; + } + backend_global = NETPLAN_BACKEND_NONE; + ovs_settings_global = (NetplanOVSSettings){0}; + return n; +} + +void +process_input_file(const char* f) +{ + GError* error = NULL; + + g_debug("Processing input file %s..", f); + if (!netplan_parse_yaml(f, &error)) { + g_fprintf(stderr, "%s\n", error->message); + exit(1); + } +} + +gboolean +process_yaml_hierarchy(const char* rootdir) +{ + glob_t gl; + /* Files with asciibetically higher names override/append settings from + * earlier ones (in all config dirs); files in /run/netplan/ + * shadow files in /etc/netplan/ which shadow files in /lib/netplan/. + * To do that, we put all found files in a hash table, then sort it by + * file name, and add the entries from /run after the ones from /etc + * and those after the ones from /lib. */ + if (find_yaml_glob(rootdir, &gl) != 0) + return FALSE; // LCOV_EXCL_LINE + /* keys are strdup()ed, free them; values point into the glob_t, don't free them */ + g_autoptr(GHashTable) configs = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + g_autoptr(GList) config_keys = NULL; + + for (size_t i = 0; i < gl.gl_pathc; ++i) + g_hash_table_insert(configs, g_path_get_basename(gl.gl_pathv[i]), gl.gl_pathv[i]); + + config_keys = g_list_sort(g_hash_table_get_keys(configs), (GCompareFunc) strcmp); + + for (GList* i = config_keys; i != NULL; i = i->next) + process_input_file(g_hash_table_lookup(configs, i->data)); + return TRUE; +} diff --git a/src/parse.h b/src/parse.h new file mode 100644 index 0000000..dc24880 --- /dev/null +++ b/src/parse.h @@ -0,0 +1,517 @@ +/* + * Copyright (C) 2016 Canonical, Ltd. + * Author: Martin Pitt + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +#define NETPLAN_VERSION_MIN 2 +#define NETPLAN_VERSION_MAX 3 + + +/* file that is currently being processed, for useful error messages */ +extern const char* current_file; + +/* List of "seen" ids not found in netdefs yet by the parser. + * These are removed when it exists in this list and we reach the point of + * creating a netdef for that id; so by the time we're done parsing the yaml + * document it should be empty. */ +extern GHashTable *missing_id; +extern int missing_ids_found; + +/**************************************************** + * Parsed definitions + ****************************************************/ + +typedef enum { + NETPLAN_DEF_TYPE_NONE, + /* physical devices */ + NETPLAN_DEF_TYPE_ETHERNET, + NETPLAN_DEF_TYPE_WIFI, + NETPLAN_DEF_TYPE_MODEM, + /* virtual devices */ + NETPLAN_DEF_TYPE_VIRTUAL, + NETPLAN_DEF_TYPE_BRIDGE = NETPLAN_DEF_TYPE_VIRTUAL, + NETPLAN_DEF_TYPE_BOND, + NETPLAN_DEF_TYPE_VLAN, + NETPLAN_DEF_TYPE_TUNNEL, + NETPLAN_DEF_TYPE_PORT, + /* Type fallback/passthrough */ + NETPLAN_DEF_TYPE_NM, + NETPLAN_DEF_TYPE_MAX_ +} NetplanDefType; + +typedef enum { + NETPLAN_BACKEND_NONE, + NETPLAN_BACKEND_NETWORKD, + NETPLAN_BACKEND_NM, + NETPLAN_BACKEND_OVS, + NETPLAN_BACKEND_MAX_, +} NetplanBackend; + +static const char* const netplan_backend_to_name[NETPLAN_BACKEND_MAX_] = { + [NETPLAN_BACKEND_NONE] = "none", + [NETPLAN_BACKEND_NETWORKD] = "networkd", + [NETPLAN_BACKEND_NM] = "NetworkManager", + [NETPLAN_BACKEND_OVS] = "OpenVSwitch", +}; + +typedef enum { + NETPLAN_RA_MODE_KERNEL, + NETPLAN_RA_MODE_ENABLED, + NETPLAN_RA_MODE_DISABLED, +} NetplanRAMode; + +typedef enum { + NETPLAN_OPTIONAL_IPV4_LL = 1<<0, + NETPLAN_OPTIONAL_IPV6_RA = 1<<1, + NETPLAN_OPTIONAL_DHCP4 = 1<<2, + NETPLAN_OPTIONAL_DHCP6 = 1<<3, + NETPLAN_OPTIONAL_STATIC = 1<<4, +} NetplanOptionalAddressFlag; + +typedef enum { + NETPLAN_ADDRGEN_DEFAULT, + NETPLAN_ADDRGEN_EUI64, + NETPLAN_ADDRGEN_STABLEPRIVACY, + NETPLAN_ADDRGEN_MAX, +} NetplanAddrGenMode; + +struct NetplanOptionalAddressType { + char* name; + NetplanOptionalAddressFlag flag; +}; + +extern struct NetplanOptionalAddressType NETPLAN_OPTIONAL_ADDRESS_TYPES[]; + +/* Tunnel mode enum; sync with NetworkManager's DBUS API */ +/* TODO: figure out whether networkd's GRETAP and NM's ISATAP + * are the same thing. + */ +typedef enum { + NETPLAN_TUNNEL_MODE_UNKNOWN = 0, + NETPLAN_TUNNEL_MODE_IPIP = 1, + NETPLAN_TUNNEL_MODE_GRE = 2, + NETPLAN_TUNNEL_MODE_SIT = 3, + NETPLAN_TUNNEL_MODE_ISATAP = 4, // NM only. + NETPLAN_TUNNEL_MODE_VTI = 5, + NETPLAN_TUNNEL_MODE_IP6IP6 = 6, + NETPLAN_TUNNEL_MODE_IPIP6 = 7, + NETPLAN_TUNNEL_MODE_IP6GRE = 8, + NETPLAN_TUNNEL_MODE_VTI6 = 9, + + /* systemd-only, apparently? */ + NETPLAN_TUNNEL_MODE_GRETAP = 101, + NETPLAN_TUNNEL_MODE_IP6GRETAP = 102, + NETPLAN_TUNNEL_MODE_WIREGUARD = 103, + + NETPLAN_TUNNEL_MODE_MAX_, +} NetplanTunnelMode; + +static const char* const +netplan_tunnel_mode_table[NETPLAN_TUNNEL_MODE_MAX_] = { + [NETPLAN_TUNNEL_MODE_UNKNOWN] = "unknown", + [NETPLAN_TUNNEL_MODE_IPIP] = "ipip", + [NETPLAN_TUNNEL_MODE_GRE] = "gre", + [NETPLAN_TUNNEL_MODE_SIT] = "sit", + [NETPLAN_TUNNEL_MODE_ISATAP] = "isatap", + [NETPLAN_TUNNEL_MODE_VTI] = "vti", + [NETPLAN_TUNNEL_MODE_IP6IP6] = "ip6ip6", + [NETPLAN_TUNNEL_MODE_IPIP6] = "ipip6", + [NETPLAN_TUNNEL_MODE_IP6GRE] = "ip6gre", + [NETPLAN_TUNNEL_MODE_VTI6] = "vti6", + + [NETPLAN_TUNNEL_MODE_GRETAP] = "gretap", + [NETPLAN_TUNNEL_MODE_IP6GRETAP] = "ip6gretap", + [NETPLAN_TUNNEL_MODE_WIREGUARD] = "wireguard", +}; + +typedef enum { + NETPLAN_WIFI_WOWLAN_DEFAULT = 1<<0, + NETPLAN_WIFI_WOWLAN_ANY = 1<<1, + NETPLAN_WIFI_WOWLAN_DISCONNECT = 1<<2, + NETPLAN_WIFI_WOWLAN_MAGIC = 1<<3, + NETPLAN_WIFI_WOWLAN_GTK_REKEY_FAILURE = 1<<4, + NETPLAN_WIFI_WOWLAN_EAP_IDENTITY_REQ = 1<<5, + NETPLAN_WIFI_WOWLAN_4WAY_HANDSHAKE = 1<<6, + NETPLAN_WIFI_WOWLAN_RFKILL_RELEASE = 1<<7, + NETPLAN_WIFI_WOWLAN_TCP = 1<<8, +} NetplanWifiWowlanFlag; + +struct NetplanWifiWowlanType { + char* name; + NetplanWifiWowlanFlag flag; +}; + +extern struct NetplanWifiWowlanType NETPLAN_WIFI_WOWLAN_TYPES[]; + +typedef enum { + NETPLAN_AUTH_KEY_MANAGEMENT_NONE, + NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK, + NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP, + NETPLAN_AUTH_KEY_MANAGEMENT_8021X, + NETPLAN_AUTH_KEY_MANAGEMENT_MAX, +} NetplanAuthKeyManagementType; + +typedef enum { + NETPLAN_AUTH_EAP_NONE, + NETPLAN_AUTH_EAP_TLS, + NETPLAN_AUTH_EAP_PEAP, + NETPLAN_AUTH_EAP_TTLS, + NETPLAN_AUTH_EAP_METHOD_MAX, +} NetplanAuthEAPMethod; + +typedef struct missing_node { + char* netdef_id; + const yaml_node_t* node; +} NetplanMissingNode; + +typedef struct authentication_settings { + NetplanAuthKeyManagementType key_management; + NetplanAuthEAPMethod eap_method; + char* identity; + char* anonymous_identity; + char* password; + char* ca_certificate; + char* client_certificate; + char* client_key; + char* client_key_password; + char* phase2_auth; /* netplan-feature: auth-phase2 */ +} NetplanAuthenticationSettings; + +/* Fields below are valid for dhcp4 and dhcp6 unless otherwise noted. */ +typedef struct dhcp_overrides { + gboolean use_dns; + gboolean use_ntp; + gboolean send_hostname; + gboolean use_hostname; + gboolean use_mtu; + gboolean use_routes; + char* use_domains; /* netplan-feature: dhcp-use-domains */ + char* hostname; + guint metric; +} NetplanDHCPOverrides; + +typedef struct ovs_controller { + char* connection_mode; + GArray* addresses; +} NetplanOVSController; + +typedef struct ovs_settings { + GHashTable* external_ids; + GHashTable* other_config; + char* lacp; + char* fail_mode; + gboolean mcast_snooping; + GArray* protocols; + gboolean rstp; + NetplanOVSController controller; + NetplanAuthenticationSettings ssl; +} NetplanOVSSettings; + +typedef union { + struct NetplanNMSettings { + char *name; + char *uuid; + char *stable_id; + char *device; + GData* passthrough; + } nm; + struct NetplanNetworkdSettings { + char *unit; + } networkd; +} NetplanBackendSettings; + +/** + * Represent a configuration stanza + */ + +struct net_definition; + +typedef struct net_definition NetplanNetDefinition; + +struct net_definition { + NetplanDefType type; + NetplanBackend backend; + char* id; + /* only necessary for NetworkManager connection UUIDs in some cases */ + uuid_t uuid; + + /* status options */ + gboolean optional; + NetplanOptionalAddressFlag optional_addresses; + gboolean critical; + + /* addresses */ + gboolean dhcp4; + gboolean dhcp6; + char* dhcp_identifier; + NetplanDHCPOverrides dhcp4_overrides; + NetplanDHCPOverrides dhcp6_overrides; + NetplanRAMode accept_ra; + GArray* ip4_addresses; + GArray* ip6_addresses; + GArray* address_options; + gboolean ip6_privacy; + guint ip6_addr_gen_mode; + char* ip6_addr_gen_token; + char* gateway4; + char* gateway6; + GArray* ip4_nameservers; + GArray* ip6_nameservers; + GArray* search_domains; + GArray* routes; + GArray* ip_rules; + GArray* wireguard_peers; + struct { + gboolean ipv4; + gboolean ipv6; + } linklocal; + + /* master ID for slave devices */ + char* bridge; + char* bond; + + /* peer ID for OVS patch ports */ + char* peer; + + /* vlan */ + guint vlan_id; + NetplanNetDefinition* vlan_link; + gboolean has_vlans; + + /* Configured custom MAC address */ + char* set_mac; + + /* interface mtu */ + guint mtubytes; + /* ipv6 mtu */ + /* netplan-feature: ipv6-mtu */ + guint ipv6_mtubytes; + + /* these properties are only valid for physical interfaces (type < ND_VIRTUAL) */ + char* set_name; + struct { + char* driver; + char* mac; + char* original_name; + } match; + gboolean has_match; + gboolean wake_on_lan; + NetplanWifiWowlanFlag wowlan; + gboolean emit_lldp; + + /* these properties are only valid for NETPLAN_DEF_TYPE_WIFI */ + GHashTable* access_points; /* SSID → NetplanWifiAccessPoint* */ + + struct { + char* mode; + char* lacp_rate; + char* monitor_interval; + guint min_links; + char* transmit_hash_policy; + char* selection_logic; + gboolean all_slaves_active; + char* arp_interval; + GArray* arp_ip_targets; + char* arp_validate; + char* arp_all_targets; + char* up_delay; + char* down_delay; + char* fail_over_mac_policy; + guint gratuitous_arp; + /* TODO: unsolicited_na */ + guint packets_per_slave; + char* primary_reselect_policy; + guint resend_igmp; + char* learn_interval; + char* primary_slave; + } bond_params; + + /* netplan-feature: modems */ + struct { + char* apn; + gboolean auto_config; + char* device_id; + char* network_id; + char* number; + char* password; + char* pin; + char* sim_id; + char* sim_operator_id; + char* username; + } modem_params; + + struct { + char* ageing_time; + guint priority; + guint port_priority; + char* forward_delay; + char* hello_time; + char* max_age; + guint path_cost; + gboolean stp; + } bridge_params; + gboolean custom_bridging; + + struct { + NetplanTunnelMode mode; + char *local_ip; + char *remote_ip; + char *input_key; + char *output_key; + char *private_key; /* used for wireguard */ + guint fwmark; + guint port; + } tunnel; + + NetplanAuthenticationSettings auth; + gboolean has_auth; + + /* these properties are only valid for SR-IOV NICs */ + /* netplan-feature: sriov */ + struct net_definition* sriov_link; + gboolean sriov_vlan_filter; + guint sriov_explicit_vf_count; + + /* these properties are only valid for OpenVSwitch */ + /* netplan-feature: openvswitch */ + NetplanOVSSettings ovs_settings; + + NetplanBackendSettings backend_settings; + + char* filename; + /* it cannot be in the tunnel struct: https://github.com/canonical/netplan/pull/206 */ + guint tunnel_ttl; + + /* netplan-feature: activation-mode */ + char* activation_mode; +}; + +typedef enum { + NETPLAN_WIFI_MODE_INFRASTRUCTURE, + NETPLAN_WIFI_MODE_ADHOC, + NETPLAN_WIFI_MODE_AP, + NETPLAN_WIFI_MODE_OTHER, + NETPLAN_WIFI_MODE_MAX_ +} NetplanWifiMode; + +static const char* const netplan_wifi_mode_to_str[NETPLAN_WIFI_MODE_MAX_] = { + [NETPLAN_WIFI_MODE_INFRASTRUCTURE] = "infrastructure", + [NETPLAN_WIFI_MODE_ADHOC] = "adhoc", + [NETPLAN_WIFI_MODE_AP] = "ap", + [NETPLAN_WIFI_MODE_OTHER] = NULL, +}; + +typedef struct { + char *endpoint; + char *public_key; + char *preshared_key; + GArray *allowed_ips; + guint keepalive; +} NetplanWireguardPeer; + +typedef enum { + NETPLAN_WIFI_BAND_DEFAULT, + NETPLAN_WIFI_BAND_5, + NETPLAN_WIFI_BAND_24 +} NetplanWifiBand; + +typedef struct { + char* address; + char* lifetime; + char* label; +} NetplanAddressOptions; + +typedef struct { + NetplanWifiMode mode; + char* ssid; + NetplanWifiBand band; + char* bssid; + gboolean hidden; + guint channel; + + NetplanAuthenticationSettings auth; + gboolean has_auth; + + NetplanBackendSettings backend_settings; +} NetplanWifiAccessPoint; + +#define NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC 0 +#define NETPLAN_CONGESTION_WINDOW_UNSPEC 0 +#define NETPLAN_MTU_UNSPEC 0 +#define NETPLAN_METRIC_UNSPEC G_MAXUINT +#define NETPLAN_ROUTE_TABLE_UNSPEC 0 +#define NETPLAN_IP_RULE_PRIO_UNSPEC G_MAXUINT +#define NETPLAN_IP_RULE_FW_MARK_UNSPEC 0 +#define NETPLAN_IP_RULE_TOS_UNSPEC G_MAXUINT + +typedef struct { + guint family; + char* type; + char* scope; + guint table; + + char* from; + char* to; + char* via; + + gboolean onlink; + + /* valid metrics are valid positive integers. + * invalid metrics are represented by METRIC_UNSPEC */ + guint metric; + + guint mtubytes; + guint congestion_window; + guint advertised_receive_window; +} NetplanIPRoute; + +typedef struct { + guint family; + + char* from; + char* to; + + /* table: Valid values are 1 <= x <= 4294967295) */ + guint table; + guint priority; + /* fwmark: Valid values are 1 <= x <= 4294967295) */ + guint fwmark; + /* type-of-service: between 0 and 255 */ + guint tos; +} NetplanIPRule; + +/* Written/updated by parse_yaml(): char* id → net_definition */ +extern GHashTable* netdefs; +extern GList* netdefs_ordered; +extern NetplanOVSSettings ovs_settings_global; + +/**************************************************** + * Functions + ****************************************************/ + +gboolean netplan_parse_yaml(const char* filename, GError** error); +GHashTable* netplan_finish_parse(GError** error); +guint netplan_clear_netdefs(); +NetplanBackend netplan_get_global_backend(); +const char* tunnel_mode_to_string(NetplanTunnelMode mode); +NetplanNetDefinition* netplan_netdef_new(const char* id, NetplanDefType type, NetplanBackend renderer); + +void process_input_file(const char* f); +gboolean process_yaml_hierarchy(const char* rootdir); diff --git a/src/sriov.c b/src/sriov.c new file mode 100644 index 0000000..60f9800 --- /dev/null +++ b/src/sriov.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2020 Canonical, Ltd. + * Author: Łukasz 'sil2100' Zemczak + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include +#include +#include + +#include "util.h" + +void +write_sriov_conf_finish(const char* rootdir) +{ + /* For now we execute apply --sriov-only everytime there is a new + SR-IOV device appearing, which is fine as it's relatively fast */ + GString *udev_rule = g_string_new("ACTION==\"add\", SUBSYSTEM==\"net\", ATTRS{sriov_totalvfs}==\"?*\", RUN+=\"/usr/sbin/netplan apply --sriov-only\"\n"); + g_string_free_to_file(udev_rule, rootdir, "run/udev/rules.d/99-sriov-netplan-setup.rules", NULL); +} + +void +cleanup_sriov_conf(const char* rootdir) +{ + g_autofree char* rulepath = g_strjoin(NULL, rootdir ?: "", "/run/udev/rules.d/99-sriov-netplan-setup.rules", NULL); + unlink(rulepath); +} diff --git a/src/sriov.h b/src/sriov.h new file mode 100644 index 0000000..7cd5896 --- /dev/null +++ b/src/sriov.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2020 Canonical, Ltd. + * Author: Łukasz 'sil2100' Zemczak + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +void write_sriov_conf_finish(const char* rootdir); +void cleanup_sriov_conf(const char* rootdir); diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..a4c0dba --- /dev/null +++ b/src/util.c @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2016 Canonical, Ltd. + * Author: Martin Pitt + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include + +#include +#include + +#include "util.h" +#include "netplan.h" + +GHashTable* wifi_frequency_24; +GHashTable* wifi_frequency_5; + +/** + * Create the parent directories of given file path. Exit program on failure. + */ +void +safe_mkdir_p_dir(const char* file_path) +{ + g_autofree char* dir = g_path_get_dirname(file_path); + + if (g_mkdir_with_parents(dir, 0755) < 0) { + g_fprintf(stderr, "ERROR: cannot create directory %s: %m\n", dir); + exit(1); + } +} + +/** + * Write a GString to a file and free it. Create necessary parent directories + * and exit with error message on error. + * @s: #GString whose contents to write. Will be fully freed afterwards. + * @rootdir: optional rootdir (@NULL means "/") + * @path: path of file to write (@rootdir will be prepended) + * @suffix: optional suffix to append to path + */ +void g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix) +{ + g_autofree char* full_path = NULL; + g_autofree char* path_suffix = NULL; + g_autofree char* contents = g_string_free(s, FALSE); + GError* error = NULL; + + path_suffix = g_strjoin(NULL, path, suffix, NULL); + full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL); + safe_mkdir_p_dir(full_path); + if (!g_file_set_contents(full_path, contents, -1, &error)) { + /* the mkdir() just succeeded, there is no sensible + * method to test this without root privileges, bind mounts, and + * simulating ENOSPC */ + // LCOV_EXCL_START + g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message); + exit(1); + // LCOV_EXCL_STOP + } +} + +/** + * Remove all files matching given glob. + */ +void +unlink_glob(const char* rootdir, const char* _glob) +{ + glob_t gl; + int rc; + g_autofree char* rglob = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, _glob, NULL); + + rc = glob(rglob, GLOB_BRACE, NULL, &gl); + if (rc != 0 && rc != GLOB_NOMATCH) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to glob for %s: %m\n", rglob); + return; + // LCOV_EXCL_STOP + } + + for (size_t i = 0; i < gl.gl_pathc; ++i) + unlink(gl.gl_pathv[i]); + globfree(&gl); +} + +/** + * Return a glob of all *.yaml files in /{lib,etc,run}/netplan/ (in this order) + */ +int find_yaml_glob(const char* rootdir, glob_t* out_glob) +{ + int rc; + g_autofree char* rglob = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "{lib,etc,run}/netplan/*.yaml", NULL); + rc = glob(rglob, GLOB_BRACE, NULL, out_glob); + if (rc != 0 && rc != GLOB_NOMATCH) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to glob for %s: %m\n", rglob); + return 1; + // LCOV_EXCL_STOP + } + + return 0; +} + +/** + * Get the frequency of a given 2.4GHz WiFi channel + */ +int +wifi_get_freq24(int channel) +{ + if (channel < 1 || channel > 14) { + g_fprintf(stderr, "ERROR: invalid 2.4GHz WiFi channel: %d\n", channel); + exit(1); + } + + if (!wifi_frequency_24) { + wifi_frequency_24 = g_hash_table_new(g_direct_hash, g_direct_equal); + /* Initialize 2.4GHz frequencies, as of: + * https://en.wikipedia.org/wiki/List_of_WLAN_channels#2.4_GHz_(802.11b/g/n/ax) */ + for (unsigned i = 0; i < 13; i++) { + g_hash_table_insert(wifi_frequency_24, GINT_TO_POINTER(i+1), + GINT_TO_POINTER(2412+i*5)); + } + g_hash_table_insert(wifi_frequency_24, GINT_TO_POINTER(14), + GINT_TO_POINTER(2484)); + } + return GPOINTER_TO_INT(g_hash_table_lookup(wifi_frequency_24, + GINT_TO_POINTER(channel))); +} + +/** + * Get the frequency of a given 5GHz WiFi channel + */ +int +wifi_get_freq5(int channel) +{ + int channels[] = { 7, 8, 9, 11, 12, 16, 32, 34, 36, 38, 40, 42, 44, 46, 48, + 50, 52, 54, 56, 58, 60, 62, 64, 68, 96, 100, 102, 104, + 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, + 128, 132, 134, 136, 138, 140, 142, 144, 149, 151, 153, + 155, 157, 159, 161, 165, 169, 173 }; + gboolean found = FALSE; + for (unsigned i = 0; i < sizeof(channels) / sizeof(int); i++) { + if (channel == channels[i]) { + found = TRUE; + break; + } + } + if (!found) { + g_fprintf(stderr, "ERROR: invalid 5GHz WiFi channel: %d\n", channel); + exit(1); + } + if (!wifi_frequency_5) { + wifi_frequency_5 = g_hash_table_new(g_direct_hash, g_direct_equal); + /* Initialize 5GHz frequencies, as of: + * https://en.wikipedia.org/wiki/List_of_WLAN_channels#5.0_GHz_(802.11j)_WLAN + * Skipping channels 183-196. They are valid only in Japan with registration needed */ + for (unsigned i = 0; i < sizeof(channels) / sizeof(int); i++) { + g_hash_table_insert(wifi_frequency_5, GINT_TO_POINTER(channels[i]), + GINT_TO_POINTER(5000+channels[i]*5)); + } + } + return GPOINTER_TO_INT(g_hash_table_lookup(wifi_frequency_5, + GINT_TO_POINTER(channel))); +} + +/** + * Systemd-escape the given string. The caller is responsible for freeing + * the allocated escaped string. + */ +gchar* +systemd_escape(char* string) +{ + g_autoptr(GError) err = NULL; + g_autofree gchar* stderrh = NULL; + gint exit_status = 0; + gchar *escaped; + + gchar *argv[] = {"bin" "/" "systemd-escape", string, NULL}; + g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &escaped, &stderrh, &exit_status, &err); + g_spawn_check_exit_status(exit_status, &err); + if (err != NULL) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", string, exit_status, escaped, stderrh); + exit(1); + // LCOV_EXCL_STOP + } + g_strstrip(escaped); + + return escaped; +} + +gboolean +netplan_delete_connection(const char* id, const char* rootdir) +{ + g_autofree gchar* filename = NULL; + g_autofree gchar* del = NULL; + g_autoptr(GError) error = NULL; + NetplanNetDefinition* nd = NULL; + + /* parse all YAML files */ + if (!process_yaml_hierarchy(rootdir)) + return FALSE; // LCOV_EXCL_LINE + + netdefs = netplan_finish_parse(&error); + if (!netdefs) { + // LCOV_EXCL_START + g_fprintf(stderr, "netplan_delete_connection: %s\n", error->message); + return FALSE; + // LCOV_EXCL_STOP + } + + /* find filename for specified netdef ID */ + nd = g_hash_table_lookup(netdefs, id); + if (!nd) { + g_warning("netplan_delete_connection: Cannot delete %s, does not exist.", id); + return FALSE; + } + + filename = g_path_get_basename(nd->filename); + filename[strlen(filename) - 5] = '\0'; //stip ".yaml" suffix + del = g_strdup_printf("network.%s.%s=NULL", netplan_def_type_to_str[nd->type], id); + netplan_clear_netdefs(); + + /* TODO: refactor logic to actually be inside the library instead of spawning another process */ + const gchar *argv[] = { SBINDIR "/" "netplan", "set", del, "--origin-hint" , filename, NULL, NULL, NULL }; + if (rootdir) { + argv[5] = "--root-dir"; + argv[6] = rootdir; + } + if (getenv("TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("TEST_NETPLAN_CMD"); + return g_spawn_sync(NULL, (gchar**)argv, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL); +} + +gboolean +netplan_generate(const char* rootdir) +{ + /* TODO: refactor logic to actually be inside the library instead of spawning another process */ + const gchar *argv[] = { SBINDIR "/" "netplan", "generate", NULL , NULL, NULL }; + if (rootdir) { + argv[2] = "--root-dir"; + argv[3] = rootdir; + } + if (getenv("TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("TEST_NETPLAN_CMD"); + return g_spawn_sync(NULL, (gchar**)argv, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL); +} + +/** + * Extract the netplan netdef ID from a NetworkManager connection profile (keyfile), + * generated by netplan. Used by the NetworkManager YAML backend. + */ +gchar* +netplan_get_id_from_nm_filename(const char* filename, const char* ssid) +{ + g_autofree gchar* escaped_ssid = NULL; + g_autofree gchar* suffix = NULL; + const char* nm_prefix = "/run/NetworkManager/system-connections/netplan-"; + const char* pos = g_strrstr(filename, nm_prefix); + const char* start = NULL; + const char* end = NULL; + gsize id_len = 0; + + if (!pos) + return NULL; + + if (ssid) { + escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE); + suffix = g_strdup_printf("-%s.nmconnection", escaped_ssid); + end = g_strrstr(filename, suffix); + } else + end = g_strrstr(filename, ".nmconnection"); + + if (!end) + return NULL; + + /* Move pointer to start of netplan ID inside filename string */ + start = pos + strlen(nm_prefix); + id_len = end - start; + return g_strndup(start, id_len); +} + +/** + * Get the filename from which the given netdef has been parsed. + * @rootdir: ID of the netdef to be looked up + * @rootdir: parse files from this root directory + */ +gchar* +netplan_get_filename_by_id(const char* netdef_id, const char* rootdir) +{ + gchar* filename = NULL; + netplan_clear_netdefs(); + if (!process_yaml_hierarchy(rootdir)) + return NULL; // LCOV_EXCL_LINE + GHashTable* netdefs = netplan_finish_parse(NULL); + if (!netdefs) + return NULL; + NetplanNetDefinition* nd = g_hash_table_lookup(netdefs, netdef_id); + if (!nd) + return NULL; + filename = g_strdup(nd->filename); + netplan_clear_netdefs(); + return filename; +} + +/** + * Get a static string describing the default global network + * for a given address family. + */ +const char * +get_global_network(int ip_family) +{ + g_assert(ip_family == AF_INET || ip_family == AF_INET6); + if (ip_family == AF_INET) + return "0.0.0.0/0"; + else + return "::/0"; +} diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..f34c601 --- /dev/null +++ b/src/util.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 Canonical, Ltd. + * Author: Martin Pitt + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define __USE_MISC +#include +#pragma once + +extern GHashTable* wifi_frequency_24; +extern GHashTable* wifi_frequency_5; + +void safe_mkdir_p_dir(const char* file_path); +void g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix); +void unlink_glob(const char* rootdir, const char* _glob); +int find_yaml_glob(const char* rootdir, glob_t* out_glob); + +const char *get_global_network(int ip_family); + +int wifi_get_freq24(int channel); +int wifi_get_freq5(int channel); + +gchar* systemd_escape(char* string); +gboolean netplan_delete_connection(const char* id, const char* rootdir); +gboolean netplan_generate(const char* rootdir); +gchar* netplan_get_id_from_nm_filename(const char* filename, const char* ssid); +gchar* netplan_get_filename_by_id(const char* netdef_id, const char* rootdir); + +#define OPENVSWITCH_OVS_VSCTL "/usr/bin/ovs-vsctl" diff --git a/src/validation.c b/src/validation.c new file mode 100644 index 0000000..a0dca68 --- /dev/null +++ b/src/validation.c @@ -0,0 +1,486 @@ +/* + * Copyright (C) 2019 Canonical, Ltd. + * Author: Mathieu Trudel-Lapierre + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include + +#include "parse.h" +#include "error.h" +#include "util.h" + + +/* Check sanity for address types */ + +gboolean +is_ip4_address(const char* address) +{ + struct in_addr a4; + int ret; + + ret = inet_pton(AF_INET, address, &a4); + g_assert(ret >= 0); + if (ret > 0) + return TRUE; + + return FALSE; +} + +gboolean +is_ip6_address(const char* address) +{ + struct in6_addr a6; + int ret; + + ret = inet_pton(AF_INET6, address, &a6); + g_assert(ret >= 0); + if (ret > 0) + return TRUE; + + return FALSE; +} + +gboolean +is_hostname(const char *hostname) +{ + static const gchar *pattern = "^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])$"; + return g_regex_match_simple(pattern, hostname, G_REGEX_CASELESS, G_REGEX_MATCH_NOTEMPTY); +} + +gboolean +is_wireguard_key(const char* key) +{ + /* Check if this is (most likely) a 265bit, base64 encoded wireguard key */ + if (strlen(key) == 44 && key[43] == '=' && key[42] != '=') { + static const gchar *pattern = "^(?:[A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=)+$"; + return g_regex_match_simple(pattern, key, 0, G_REGEX_MATCH_NOTEMPTY); + } + return FALSE; +} + +/* Check sanity of OpenVSwitch controller targets */ +gboolean +validate_ovs_target(gboolean host_first, gchar* s) { + static guint dport = 6653; // the default port + g_autofree gchar* host = NULL; + g_autofree gchar* port = NULL; + gchar** vec = NULL; + + /* Format tcp:host[:port] or ssl:host[:port] */ + if (host_first) { + g_assert(s != NULL); + // IP6 host, indicated by bracketed notation ([..IPv6..]) + if (s[0] == '[') { + gchar* tmp = NULL; + tmp = s+1; //get rid of leading '[' + // append default port to unify parsing + if (!g_strrstr(tmp, "]:")) + vec = g_strsplit(g_strdup_printf("%s:%u", tmp, dport), "]:", 2); + else + vec = g_strsplit(tmp, "]:", 2); + // IP4 host + } else { + // append default port to unify parsing + if (!g_strrstr(s, ":")) + vec = g_strsplit(g_strdup_printf("%s:%u", s, dport), ":", 2); + else + vec = g_strsplit(s, ":", 2); + } + // host and port are always set + host = g_strdup(vec[0]); //set host alias + port = g_strdup(vec[1]); //set port alias + g_assert(vec[2] == NULL); + g_strfreev(vec); + /* Format ptcp:[port][:host] or pssl:[port][:host] */ + } else { + // special case: "ptcp:" (no port, no host) + if (!g_strcmp0(s, "")) + port = g_strdup_printf("%u", dport); + else { + vec = g_strsplit(s, ":", 2); + port = g_strdup(vec[0]); + host = g_strdup(vec[1]); + // get rid of leading & trailing IPv6 brackets + if (host && host[0] == '[') { + char **split = g_strsplit_set(host, "[]", 3); + g_free(host); + host = g_strjoinv("", split); + g_strfreev(split); + } + g_strfreev(vec); + } + } + + g_assert(port != NULL); + // special case where IPv6 notation contains '%iface' name + if (host && g_strrstr(host, "%")) { + gchar** split = g_strsplit (host, "%", 2); + g_free(host); + host = g_strdup(split[0]); // designated scope for IPv6 link-level addresses + g_assert(split[1] != NULL && split[2] == NULL); + g_strfreev(split); + } + + if (atoi(port) > 0 && atoi(port) <= 65535) { + if (!host) + return TRUE; + else if (host && (is_ip4_address(host) || is_ip6_address(host))) + return TRUE; + } + return FALSE; +} + +/************************************************ + * Validation for grammar and backend rules. + ************************************************/ +static gboolean +validate_tunnel_key(yaml_node_t* node, gchar* key, GError** error) +{ + /* Tunnel key should be a number or dotted quad, except for wireguard. */ + gchar* endptr; + guint64 v = g_ascii_strtoull(key, &endptr, 10); + if (*endptr != '\0' || v > G_MAXUINT) { + /* Not a simple uint, try for a dotted quad */ + if (!is_ip4_address(key)) + return yaml_error(node, error, "invalid tunnel key '%s'", key); + } + return TRUE; +} + +static gboolean +validate_tunnel_grammar(NetplanNetDefinition* nd, yaml_node_t* node, GError** error) +{ + if (nd->tunnel.mode == NETPLAN_TUNNEL_MODE_UNKNOWN) + return yaml_error(node, error, "%s: missing 'mode' property for tunnel", nd->id); + + if (nd->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) { + if (!nd->tunnel.private_key) + return yaml_error(node, error, "%s: missing 'key' property (private key) for wireguard", nd->id); + if (nd->tunnel.private_key[0] != '/' && !is_wireguard_key(nd->tunnel.private_key)) + return yaml_error(node, error, "%s: invalid wireguard private key", nd->id); + if (!nd->wireguard_peers || nd->wireguard_peers->len == 0) + return yaml_error(node, error, "%s: at least one peer is required.", nd->id); + for (guint i = 0; i < nd->wireguard_peers->len; i++) { + NetplanWireguardPeer *peer = g_array_index (nd->wireguard_peers, NetplanWireguardPeer*, i); + + if (!peer->public_key) + return yaml_error(node, error, "%s: keys.public is required.", nd->id); + if (!is_wireguard_key(peer->public_key)) + return yaml_error(node, error, "%s: invalid wireguard public key", nd->id); + if (peer->preshared_key && peer->preshared_key[0] != '/' && !is_wireguard_key(peer->preshared_key)) + return yaml_error(node, error, "%s: invalid wireguard shared key", nd->id); + if (!peer->allowed_ips || peer->allowed_ips->len == 0) + return yaml_error(node, error, "%s: 'to' is required to define the allowed IPs.", nd->id); + if (peer->keepalive > 65535) + return yaml_error(node, error, "%s: keepalive must be 0-65535 inclusive.", nd->id); + } + return TRUE; + } else { + if (nd->tunnel.input_key && !validate_tunnel_key(node, nd->tunnel.input_key, error)) + return FALSE; + if (nd->tunnel.output_key && !validate_tunnel_key(node, nd->tunnel.output_key, error)) + return FALSE; + } + + /* Validate local/remote IPs */ + if (!nd->tunnel.local_ip) + return yaml_error(node, error, "%s: missing 'local' property for tunnel", nd->id); + if (!nd->tunnel.remote_ip) + return yaml_error(node, error, "%s: missing 'remote' property for tunnel", nd->id); + if (nd->tunnel_ttl && nd->tunnel_ttl > 255) + return yaml_error(node, error, "%s: 'ttl' property for tunnel must be in range [1...255]", nd->id); + + switch(nd->tunnel.mode) { + case NETPLAN_TUNNEL_MODE_IPIP6: + case NETPLAN_TUNNEL_MODE_IP6IP6: + case NETPLAN_TUNNEL_MODE_IP6GRE: + case NETPLAN_TUNNEL_MODE_IP6GRETAP: + case NETPLAN_TUNNEL_MODE_VTI6: + if (!is_ip6_address(nd->tunnel.local_ip)) + return yaml_error(node, error, "%s: 'local' must be a valid IPv6 address for this tunnel type", nd->id); + if (!is_ip6_address(nd->tunnel.remote_ip)) + return yaml_error(node, error, "%s: 'remote' must be a valid IPv6 address for this tunnel type", nd->id); + break; + + default: + if (!is_ip4_address(nd->tunnel.local_ip)) + return yaml_error(node, error, "%s: 'local' must be a valid IPv4 address for this tunnel type", nd->id); + if (!is_ip4_address(nd->tunnel.remote_ip)) + return yaml_error(node, error, "%s: 'remote' must be a valid IPv4 address for this tunnel type", nd->id); + break; + } + + return TRUE; +} + +static gboolean +validate_tunnel_backend_rules(NetplanNetDefinition* nd, yaml_node_t* node, GError** error) +{ + /* Backend-specific validation rules for tunnels */ + switch (nd->backend) { + case NETPLAN_BACKEND_NETWORKD: + switch (nd->tunnel.mode) { + case NETPLAN_TUNNEL_MODE_VTI: + case NETPLAN_TUNNEL_MODE_VTI6: + case NETPLAN_TUNNEL_MODE_WIREGUARD: + break; + + /* TODO: Remove this exception and fix ISATAP handling with the + * networkd backend. + * systemd-networkd has grown ISATAP support in 918049a. + */ + case NETPLAN_TUNNEL_MODE_ISATAP: + return yaml_error(node, error, + "%s: %s tunnel mode is not supported by networkd", + nd->id, + g_ascii_strup(tunnel_mode_to_string(nd->tunnel.mode), -1)); + break; + + default: + if (nd->tunnel.input_key) + return yaml_error(node, error, "%s: 'input-key' is not required for this tunnel type", nd->id); + if (nd->tunnel.output_key) + return yaml_error(node, error, "%s: 'output-key' is not required for this tunnel type", nd->id); + break; + } + break; + + case NETPLAN_BACKEND_NM: + switch (nd->tunnel.mode) { + case NETPLAN_TUNNEL_MODE_GRE: + case NETPLAN_TUNNEL_MODE_IP6GRE: + case NETPLAN_TUNNEL_MODE_WIREGUARD: + break; + + case NETPLAN_TUNNEL_MODE_GRETAP: + case NETPLAN_TUNNEL_MODE_IP6GRETAP: + return yaml_error(node, error, + "%s: %s tunnel mode is not supported by NetworkManager", + nd->id, + g_ascii_strup(tunnel_mode_to_string(nd->tunnel.mode), -1)); + break; + + default: + if (nd->tunnel.input_key) + return yaml_error(node, error, "%s: 'input-key' is not required for this tunnel type", nd->id); + if (nd->tunnel.output_key) + return yaml_error(node, error, "%s: 'output-key' is not required for this tunnel type", nd->id); + break; + } + break; + + default: break; //LCOV_EXCL_LINE + } + + return TRUE; +} + +gboolean +validate_netdef_grammar(NetplanNetDefinition* nd, yaml_node_t* node, GError** error) +{ + int missing_id_count = g_hash_table_size(missing_id); + gboolean valid = FALSE; + + g_assert(nd->type != NETPLAN_DEF_TYPE_NONE); + + /* Skip all validation if we're missing some definition IDs (devices). + * The ones we have yet to see may be necessary for validation to succeed, + * we can complete it on the next parser pass. */ + if (missing_id_count > 0) + return TRUE; + + /* set-name: requires match: */ + if (nd->set_name && !nd->has_match) + return yaml_error(node, error, "%s: 'set-name:' requires 'match:' properties", nd->id); + + if (nd->type == NETPLAN_DEF_TYPE_WIFI && nd->access_points == NULL) + return yaml_error(node, error, "%s: No access points defined", nd->id); + + if (nd->type == NETPLAN_DEF_TYPE_VLAN) { + if (!nd->vlan_link) + return yaml_error(node, error, "%s: missing 'link' property", nd->id); + nd->vlan_link->has_vlans = TRUE; + if (nd->vlan_id == G_MAXUINT) + return yaml_error(node, error, "%s: missing 'id' property", nd->id); + if (nd->vlan_id > 4094) + return yaml_error(node, error, "%s: invalid id '%u' (allowed values are 0 to 4094)", nd->id, nd->vlan_id); + } + + if (nd->type == NETPLAN_DEF_TYPE_TUNNEL) { + valid = validate_tunnel_grammar(nd, node, error); + if (!valid) + goto netdef_grammar_error; + } + + if (nd->ip6_addr_gen_mode != NETPLAN_ADDRGEN_DEFAULT && nd->ip6_addr_gen_token) + return yaml_error(node, error, "%s: ipv6-address-generation and ipv6-address-token are mutually exclusive", nd->id); + + if (nd->backend == NETPLAN_BACKEND_OVS) { + // LCOV_EXCL_START + if (!g_file_test(OPENVSWITCH_OVS_VSCTL, G_FILE_TEST_EXISTS)) { + /* Tested via integration test */ + return yaml_error(node, error, "%s: The 'ovs-vsctl' tool is required to setup OpenVSwitch interfaces.", nd->id); + } + // LCOV_EXCL_STOP + } + + if (nd->type == NETPLAN_DEF_TYPE_NM && (!nd->backend_settings.nm.passthrough || !g_datalist_get_data(&nd->backend_settings.nm.passthrough, "connection.type"))) + return yaml_error(node, error, "%s: network type 'nm-devices:' needs to provide a 'connection.type' via passthrough", nd->id); + + valid = TRUE; + +netdef_grammar_error: + return valid; +} + +gboolean +validate_backend_rules(NetplanNetDefinition* nd, GError** error) +{ + gboolean valid = FALSE; + /* Set a dummy, NULL yaml_node_t for error reporting */ + yaml_node_t* node = NULL; + + g_assert(nd->type != NETPLAN_DEF_TYPE_NONE); + + if (nd->type == NETPLAN_DEF_TYPE_TUNNEL) { + valid = validate_tunnel_backend_rules(nd, node, error); + if (!valid) + goto backend_rules_error; + } + + valid = TRUE; + +backend_rules_error: + return valid; +} + +struct _defroute_entry { + int family; + int table; + int metric; + const char *netdef_id; +}; + +static void +defroute_err(struct _defroute_entry *entry, const char *new_netdef_id, GError **error) { + char table_name[128] = {}; + char metric_name[128] = {}; + + g_assert(entry->family == AF_INET || entry->family == AF_INET6); + + // XXX: handle 254 as an alias for main ? + if (entry->table == NETPLAN_ROUTE_TABLE_UNSPEC) + strncpy(table_name, "table: main", sizeof(table_name) - 1); + else + snprintf(table_name, sizeof(table_name) - 1, "table: %d", entry->table); + + if (entry->metric == NETPLAN_METRIC_UNSPEC) + strncpy(metric_name, "metric: default", sizeof(metric_name) - 1); + else + snprintf(metric_name, sizeof(metric_name) - 1, "metric: %d", entry->metric); + + g_set_error(error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT, + "Conflicting default route declarations for %s (%s, %s), first declared in %s but also in %s", + (entry->family == AF_INET) ? "IPv4" : "IPv6", + table_name, + metric_name, + entry->netdef_id, + new_netdef_id); +} + +static gboolean +check_defroute(struct _defroute_entry *candidate, + GSList **entries, + GError **error) +{ + struct _defroute_entry *entry; + GSList *it; + + g_assert(entries != NULL); + it = *entries; + + while (it) { + struct _defroute_entry *e = it->data; + if (e->family == candidate->family && + e->table == candidate->table && + e->metric == candidate->metric) { + defroute_err(e, candidate->netdef_id, error); + return FALSE; + } + it = it->next; + } + entry = g_malloc(sizeof(*entry)); + *entry = *candidate; + *entries = g_slist_prepend(*entries, entry); + return TRUE; +} + +gboolean +validate_default_route_consistency(GHashTable *netdefs, GError ** error) +{ + struct _defroute_entry candidate = {}; + GSList *defroutes = NULL; + gboolean ret = TRUE; + gpointer key, value; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, netdefs); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + NetplanNetDefinition *nd = value; + candidate.netdef_id = key; + candidate.metric = NETPLAN_METRIC_UNSPEC; + candidate.table = NETPLAN_ROUTE_TABLE_UNSPEC; + if (nd->gateway4) { + candidate.family = AF_INET; + if (!check_defroute(&candidate, &defroutes, error)) { + ret = FALSE; + break; + } + } + if (nd->gateway6) { + candidate.family = AF_INET6; + if (!check_defroute(&candidate, &defroutes, error)) { + ret = FALSE; + break; + } + } + + if (!nd->routes) + continue; + + for (size_t i = 0; i < nd->routes->len; i++) { + NetplanIPRoute* r = g_array_index(nd->routes, NetplanIPRoute*, i); + char *suffix = strrchr(r->to, '/'); + if (g_strcmp0(suffix, "/0") == 0 || g_strcmp0(r->to, "default") == 0) { + candidate.family = r->family; + candidate.table = r->table; + candidate.metric = r->metric; + if (!check_defroute(&candidate, &defroutes, error)) { + ret = FALSE; + break; + } + } + } + } + g_slist_free_full(defroutes, g_free); + return ret; +} diff --git a/src/validation.h b/src/validation.h new file mode 100644 index 0000000..3f6e527 --- /dev/null +++ b/src/validation.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 Canonical, Ltd. + * Author: Mathieu Trudel-Lapierre + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "parse.h" +#include + + +gboolean is_ip4_address(const char* address); +gboolean is_ip6_address(const char* address); +gboolean is_hostname(const char* hostname); +gboolean is_wireguard_key(const char* hostname); +gboolean validate_ovs_target(gboolean host_first, gchar* s); + +gboolean +validate_netdef_grammar(NetplanNetDefinition* nd, yaml_node_t* node, GError** error); + +gboolean +validate_backend_rules(NetplanNetDefinition* nd, GError** error); + +gboolean +validate_default_route_consistency(GHashTable* netdefs, GError** error); diff --git a/tests/cli.py b/tests/cli.py new file mode 100755 index 0000000..ee00ecc --- /dev/null +++ b/tests/cli.py @@ -0,0 +1,692 @@ +#!/usr/bin/python3 +# Blackbox tests of netplan CLI. These are run during "make check" and don't +# touch the system configuration at all. +# +# Copyright (C) 2016 Canonical, Ltd. +# Author: Martin Pitt +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import subprocess +import unittest +import tempfile +import shutil + +import yaml + +rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +exe_cli = [os.path.join(rootdir, 'src', 'netplan.script')] +if shutil.which('python3-coverage'): + exe_cli = ['python3-coverage', 'run', '--append', '--'] + exe_cli + +# Make sure we can import our development netplan. +os.environ.update({'PYTHONPATH': '.'}) +os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) + + +def _load_yaml(text): + return yaml.load(text, Loader=yaml.SafeLoader) + + +class TestArgs(unittest.TestCase): + '''Generic argument parsing tests''' + + def test_global_help(self): + out = subprocess.check_output(exe_cli + ['--help']) + self.assertIn(b'Available commands', out) + self.assertIn(b'generate', out) + self.assertIn(b'--debug', out) + + def test_command_help(self): + out = subprocess.check_output(exe_cli + ['generate', '--help']) + self.assertIn(b'--root-dir', out) + + def test_no_command(self): + os.environ['NETPLAN_GENERATE_PATH'] = os.path.join(rootdir, 'generate') + p = subprocess.Popen(exe_cli, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (out, err) = p.communicate() + self.assertEqual(out, b'') + self.assertIn(b'need to specify a command', err) + self.assertNotEqual(p.returncode, 0) + + +class TestGenerate(unittest.TestCase): + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + + def test_no_config(self): + p = subprocess.Popen(exe_cli + ['generate', '--root-dir', self.workdir.name], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (out, err) = p.communicate() + self.assertEqual(out, b'') + self.assertEqual(os.listdir(self.workdir.name), ['run']) + + def test_with_empty_config(self): + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + open(os.path.join(c, 'a.yaml'), 'w').close() + with open(os.path.join(c, 'b.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: + enlol: {dhcp4: yes}''') + out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name], stderr=subprocess.STDOUT) + self.assertEqual(out, b'') + self.assertEqual(os.listdir(os.path.join(self.workdir.name, 'run', 'systemd', 'network')), + ['10-netplan-enlol.network']) + + def test_with_config(self): + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + with open(os.path.join(c, 'a.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: + enlol: {dhcp4: yes}''') + out = subprocess.check_output(exe_cli + ['generate', '--root-dir', self.workdir.name]) + self.assertEqual(out, b'') + self.assertEqual(os.listdir(os.path.join(self.workdir.name, 'run', 'systemd', 'network')), + ['10-netplan-enlol.network']) + + def test_mapping_for_unknown_iface(self): + os.environ['NETPLAN_GENERATE_PATH'] = os.path.join(rootdir, 'generate') + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + with open(os.path.join(c, 'a.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: + enlol: {dhcp4: yes}''') + p = subprocess.Popen(exe_cli + + ['generate', '--root-dir', self.workdir.name, '--mapping', 'nonexistent'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = p.communicate() + self.assertNotEqual(p.returncode, 0) + self.assertNotIn(b'nonexistent', out) + + def test_mapping_for_interface(self): + os.environ['NETPLAN_GENERATE_PATH'] = os.path.join(rootdir, 'generate') + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + with open(os.path.join(c, 'a.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: + enlol: {dhcp4: yes}''') + out = subprocess.check_output(exe_cli + + ['generate', '--root-dir', self.workdir.name, '--mapping', 'enlol']) + self.assertNotEqual(b'', out) + self.assertIn('enlol', out.decode('utf-8')) + + def test_mapping_for_renamed_iface(self): + os.environ['NETPLAN_GENERATE_PATH'] = os.path.join(rootdir, 'generate') + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + with open(os.path.join(c, 'a.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: + myif: + match: + name: enlol + set-name: renamediface + dhcp4: yes +''') + out = subprocess.check_output(exe_cli + + ['generate', '--root-dir', self.workdir.name, '--mapping', 'renamediface']) + self.assertNotEqual(b'', out) + self.assertIn('renamediface', out.decode('utf-8')) + + +class TestIfupdownMigrate(unittest.TestCase): + + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + self.ifaces_path = os.path.join(self.workdir.name, 'etc/network/interfaces') + self.converted_path = os.path.join(self.workdir.name, 'etc/netplan/10-ifupdown.yaml') + + def test_system(self): + os.environ.update({"ENABLE_TEST_COMMANDS": "1"}) + rc = subprocess.call(exe_cli + ['migrate', '--dry-run'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # may succeed or fail, but should not crash + self.assertIn(rc, [0, 2]) + + def do_test(self, iface_file, expect_success=True, dry_run=True, dropins=None): + os.environ.update({"ENABLE_TEST_COMMANDS": "1"}) + if iface_file is not None: + os.makedirs(os.path.dirname(self.ifaces_path)) + with open(self.ifaces_path, 'w') as f: + f.write(iface_file) + if dropins: + for fname, contents in dropins.items(): + path = os.path.join(os.path.dirname(self.ifaces_path), fname) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, 'w') as f: + f.write(contents) + + argv = exe_cli + ['--debug', 'migrate', '--root-dir', self.workdir.name] + if dry_run: + argv.append('--dry-run') + p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (out, err) = p.communicate() + if expect_success: + self.assertEqual(p.returncode, 0, err.decode()) + else: + self.assertIn(p.returncode, [2, 3], err.decode()) + return (out, err) + + # + # configs which can be converted + # + + def test_no_config(self): + (out, err) = self.do_test(None) + self.assertEqual(out, b'') + self.assertEqual(os.listdir(self.workdir.name), []) + + def test_only_empty_include(self): + out = self.do_test('''# default interfaces file +source-directory /etc/network/interfaces.d''')[0] + self.assertFalse(os.path.exists(self.converted_path)) + self.assertEqual(out, b'') + + def test_loopback_only(self): + (out, err) = self.do_test('auto lo\n#ignore me\niface lo inet loopback') + self.assertEqual(out, b'') + self.assertIn(b'nothing to migrate\n', err) + + def test_dhcp4(self): + out = self.do_test('auto en1\niface en1 inet dhcp')[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) + + def test_dhcp6(self): + out = self.do_test('auto en1\niface en1 inet6 dhcp')[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp6': True}}}}, out.decode()) + + def test_dhcp4_and_6(self): + out = self.do_test('auto lo\niface lo inet loopback\n\n' + 'auto en1\niface en1 inet dhcp\niface en1 inet6 dhcp')[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True, 'dhcp6': True}}}}, out.decode()) + + def test_includedir_rel(self): + out = self.do_test('iface lo inet loopback\nauto lo\nsource-directory interfaces.d', + dropins={'interfaces.d/std': 'auto en1\niface en1 inet dhcp', + 'interfaces.d/std.bak': 'some_bogus dontreadme'})[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) + + def test_includedir_abs(self): + out = self.do_test('iface lo inet loopback\nauto lo\nsource-directory /etc/network/defs/my', + dropins={'defs/my/std': 'auto en1\niface en1 inet dhcp', + 'defs/my/std.bak': 'some_bogus dontreadme'})[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) + + def test_include_rel(self): + out = self.do_test('iface lo inet loopback\nauto lo\nsource interfaces.d/*.cfg', + dropins={'interfaces.d/std.cfg': 'auto en1\niface en1 inet dhcp', + 'interfaces.d/std.cfgold': 'some_bogus dontreadme'})[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) + + def test_include_abs(self): + out = self.do_test('iface lo inet loopback\nauto lo\nsource /etc/network/*.cfg', + dropins={'std.cfg': 'auto en1\niface en1 inet dhcp', + 'std.cfgold': 'some_bogus dontreadme'})[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) + + def test_allow(self): + out = self.do_test('allow-hotplug en1\niface en1 inet dhcp\n' + 'allow-auto en2\niface en2 inet dhcp')[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True}, + 'en2': {'dhcp4': True}}}}, out.decode()) + + def test_no_scripts(self): + out = self.do_test('auto en1\niface en1 inet dhcp\nno-scripts en1')[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True}}}}, out.decode()) + + def test_write_file_noconfig(self): + (out, err) = self.do_test('auto lo\niface lo inet loopback', dry_run=False) + self.assertFalse(os.path.exists(self.converted_path)) + # should disable original ifupdown config + self.assertFalse(os.path.exists(self.ifaces_path)) + self.assertTrue(os.path.exists(self.ifaces_path + '.netplan-converted')) + + def test_write_file_haveconfig(self): + (out, err) = self.do_test('auto en1\niface en1 inet dhcp', dry_run=False) + with open(self.converted_path) as f: + config = _load_yaml(f) + self.assertEqual(config, {'network': { + 'version': 2, + 'ethernets': {'en1': {'dhcp4': True}}}}) + + # should disable original ifupdown config + self.assertFalse(os.path.exists(self.ifaces_path)) + self.assertTrue(os.path.exists(self.ifaces_path + '.netplan-converted')) + + def test_write_file_prev_run(self): + os.makedirs(os.path.dirname(self.converted_path)) + with open(self.converted_path, 'w') as f: + f.write('canary') + (out, err) = self.do_test('auto en1\niface en1 inet dhcp', dry_run=False, expect_success=False) + with open(self.converted_path) as f: + self.assertEqual(f.read(), 'canary') + + # should not disable original ifupdown config + self.assertTrue(os.path.exists(self.ifaces_path)) + + # + # static + # + + def test_static_ipv4_prefix(self): + out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"]}}}}, out.decode()) + + def test_static_ipv4_netmask(self): + out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4\nnetmask 255.0.0.0', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"]}}}}, out.decode()) + + def test_static_ipv4_no_address(self): + out, err = self.do_test('auto en1\niface en1 inet static\nnetmask 1.2.3.4', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'no address supplied', err) + + def test_static_ipv4_no_network(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'does not specify prefix length, and netmask not specified', err) + + def test_static_ipv4_invalid_addr(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.400/8', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'error parsing "1.2.3.400" as an IPv4 address', err) + + def test_static_ipv4_invalid_netmask(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4\nnetmask 123.123.123.0', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'error parsing "1.2.3.4/123.123.123.0" as an IPv4 network', err) + + def test_static_ipv4_invalid_prefixlen(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/42', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'error parsing "1.2.3.4/42" as an IPv4 network', err) + + def test_static_ipv4_unsupported_option(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/24\nmetric 1280', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'unsupported inet option "metric"', err) + + def test_static_ipv4_unknown_option(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/24\nxyzzy 1280', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'unknown inet option "xyzzy"', err) + + def test_static_ipv6_prefix(self): + out = self.do_test('auto en1\niface en1 inet6 static\naddress fc00:0123:4567:89ab:cdef::1234/64', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["fc00:123:4567:89ab:cdef::1234/64"]}}}}, out.decode()) + + def test_static_ipv6_netmask(self): + out = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234\nnetmask 64', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["fc00:123:4567:89ab:cdef::1234/64"]}}}}, out.decode()) + + def test_static_ipv6_no_address(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\nnetmask 64', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'no address supplied', err) + + def test_static_ipv6_no_network(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'does not specify prefix length, and netmask not specified', err) + + def test_static_ipv6_invalid_addr(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::12345/64', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'error parsing "fc00:0123:4567:89ab:cdef::12345" as an IPv6 address', err) + + def test_static_ipv6_invalid_netmask(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234\nnetmask 129', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'error parsing "fc00:0123:4567:89ab:cdef::1234/129" as an IPv6 network', err) + + def test_static_ipv6_invalid_prefixlen(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234/129', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'error parsing "fc00:0123:4567:89ab:cdef::1234/129" as an IPv6 network', err) + + def test_static_ipv6_unsupported_option(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234/64\nmetric 1280', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'unsupported inet6 option "metric"', err) + + def test_static_ipv6_unknown_option(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234/64\nxyzzy 1280', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'unknown inet6 option "xyzzy"', err) + + def test_static_ipv6_accept_ra_0(self): + out = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234/64\naccept_ra 0', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["fc00:123:4567:89ab:cdef::1234/64"], + 'accept_ra': False}}}}, out.decode()) + + def test_static_ipv6_accept_ra_1(self): + out = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234/64\naccept_ra 1', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["fc00:123:4567:89ab:cdef::1234/64"], + 'accept_ra': True}}}}, out.decode()) + + def test_static_ipv6_accept_ra_2(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234/64\naccept_ra 2', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'netplan does not support accept_ra=2', err) + + def test_static_ipv6_accept_ra_unexpected(self): + out, err = self.do_test('auto en1\niface en1 inet6 static\n' + 'address fc00:0123:4567:89ab:cdef::1234/64\naccept_ra fish', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'unexpected accept_ra value "fish"', err) + + def test_static_gateway(self): + out = self.do_test("""auto en1 +iface en1 inet static + address 1.2.3.4 + netmask 255.0.0.0 + gateway 1.1.1.1 +iface en1 inet6 static + address fc00:0123:4567:89ab:cdef::1234/64 + gateway fc00:0123:4567:89ab::1""", dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': + {'addresses': ["1.2.3.4/8", "fc00:123:4567:89ab:cdef::1234/64"], + 'gateway4': "1.1.1.1", + 'gateway6': "fc00:0123:4567:89ab::1"}}}}, out.decode()) + + def test_static_dns(self): + out = self.do_test("""auto en1 +iface en1 inet static + address 1.2.3.4 + netmask 255.0.0.0 + dns-nameservers 1.2.1.1 1.2.2.1 + dns-search weird.network +iface en1 inet6 static + address fc00:0123:4567:89ab:cdef::1234/64 + dns-nameservers fc00:0123:4567:89ab:1::1 fc00:0123:4567:89ab:2::1""", dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': + {'addresses': ["1.2.3.4/8", "fc00:123:4567:89ab:cdef::1234/64"], + 'nameservers': { + 'search': ['weird.network'], + 'addresses': ['1.2.1.1', '1.2.2.1', + 'fc00:0123:4567:89ab:1::1', 'fc00:0123:4567:89ab:2::1'] + }}}}}, out.decode()) + + def test_static_dns2(self): + out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\ndns-search foo foo.bar', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"], + 'nameservers': { + 'search': ['foo', 'foo.bar'] + }}}}}, out.decode()) + + def test_static_mtu(self): + out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nmtu 1280', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"], + 'mtu': 1280}}}}, out.decode()) + + def test_static_invalid_mtu(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nmtu fish', expect_success=False) + self.assertEqual(b'', out) + self.assertIn(b'cannot parse "fish" as an MTU', err) + + def test_static_two_different_mtus(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nmtu 1280\n' + 'iface en1 inet6 static\naddress 2001::1/64\nmtu 9000', expect_success=False) + self.assertEqual(b'', out) + self.assertIn(b'tried to set MTU=9000, but already have MTU=1280', err) + + def test_static_hwaddress(self): + out = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nhwaddress 52:54:00:6b:3c:59', dry_run=True)[0] + self.assertEqual(_load_yaml(out), {'network': { + 'version': 2, + 'ethernets': {'en1': {'addresses': ["1.2.3.4/8"], + 'macaddress': '52:54:00:6b:3c:59'}}}}, out.decode()) + + def test_static_two_different_macs(self): + out, err = self.do_test('auto en1\niface en1 inet static\naddress 1.2.3.4/8\nhwaddress 52:54:00:6b:3c:59\n' + 'iface en1 inet6 static\naddress 2001::1/64\nhwaddress 52:54:00:6b:3c:58', expect_success=False) + self.assertEqual(b'', out) + self.assertIn(b'tried to set MAC 52:54:00:6b:3c:58, but already have MAC 52:54:00:6b:3c:59', err) + + # + # configs which are not supported + # + + def test_noauto(self): + (out, err) = self.do_test('iface en1 inet dhcp', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'non-automatic interfaces are not supported', err) + + def test_dhcp_options(self): + (out, err) = self.do_test('auto en1\niface en1 inet dhcp\nup myhook', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'option(s) up are not supported for dhcp method', err) + + def test_mapping(self): + (out, err) = self.do_test('mapping en*\n script /some/path/mapscheme\nmap HOME en1-home\n\n' + 'auto map1\niface map1 inet dhcp', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'mapping stanza is not supported', err) + + def test_unknown_allow(self): + (out, err) = self.do_test('allow-foo en1\niface en1 inet dhcp', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'Unknown stanza type allow-foo', err) + + def test_unknown_stanza(self): + (out, err) = self.do_test('foo en1\niface en1 inet dhcp', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'Unknown stanza type foo', err) + + def test_unknown_family(self): + (out, err) = self.do_test('auto en1\niface en1 inet7 dhcp', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'Unknown address family inet7', err) + + def test_unknown_method(self): + (out, err) = self.do_test('auto en1\niface en1 inet mangle', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'Unsupported method mangle', err) + + def test_too_few_fields(self): + (out, err) = self.do_test('auto en1\niface en1 inet', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'Expected 3 fields for stanza type iface but got 2', err) + + def test_too_many_fields(self): + (out, err) = self.do_test('auto en1\niface en1 inet dhcp foo', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'Expected 3 fields for stanza type iface but got 4', err) + + def test_write_file_unsupported(self): + (out, err) = self.do_test('iface en1 inet dhcp', expect_success=False) + self.assertEqual(out, b'') + self.assertIn(b'non-automatic interfaces are not supported', err) + # should keep original ifupdown config + self.assertTrue(os.path.exists(self.ifaces_path)) + + +class TestInfo(unittest.TestCase): + '''Test netplan info''' + + def test_info_defaults(self): + """ + Check that 'netplan info' outputs at all, should include website URL + """ + out = subprocess.check_output(exe_cli + ['info']) + self.assertIn(b'features:', out) + + def test_info_yaml(self): + """ + Verify that 'netplan info --yaml' output looks a bit like YAML + """ + out = subprocess.check_output(exe_cli + ['info', '--yaml']) + self.assertIn(b'features:', out) + + def test_info_json(self): + """ + Verify that 'netplan info --json' output looks a bit like JSON + """ + out = subprocess.check_output(exe_cli + ['info', '--json']) + self.assertIn(b'"features": [', out) + + +class TestIp(unittest.TestCase): + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + + def test_valid_subcommand(self): + p = subprocess.Popen(exe_cli + ['ip'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (out, err) = p.communicate() + self.assertEqual(out, b'') + self.assertIn(b'Available command', err) + self.assertNotEqual(p.returncode, 0) + + def test_ip_leases_networkd(self): + os.environ['NETPLAN_GENERATE_PATH'] = os.path.join(rootdir, 'generate') + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + with open(os.path.join(c, 'a.yaml'), 'w') as f: + # match against loopback so as to successfully get a predictable + # ifindex + f.write('''network: + version: 2 + renderer: networkd + ethernets: + enlol: + match: + name: lo + dhcp4: yes +''') + fake_netif_lease_dir = os.path.join(self.workdir.name, + 'run', 'systemd', 'netif', 'leases') + os.makedirs(fake_netif_lease_dir) + with open(os.path.join(fake_netif_lease_dir, '1'), 'w') as f: + f.write('''THIS IS A FAKE NETIF LEASE FOR LO''') + out = subprocess.check_output(exe_cli + + ['ip', 'leases', + '--root-dir', self.workdir.name, 'lo']) + self.assertNotEqual(out, b'') + self.assertIn('FAKE NETIF', out.decode('utf-8')) + + def test_ip_leases_nm(self): + unittest.skip("Cannot be tested offline due to calls required to nmcli." + "This is tested in integration tests.") + + def test_ip_leases_no_networkd_lease(self): + os.environ['NETPLAN_GENERATE_PATH'] = os.path.join(rootdir, 'generate') + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + with open(os.path.join(c, 'a.yaml'), 'w') as f: + # match against loopback so as to successfully get a predictable + # ifindex + f.write('''network: + version: 2 + ethernets: + enlol: + match: + name: lo + dhcp4: yes +''') + p = subprocess.Popen(exe_cli + + ['ip', 'leases', '--root-dir', self.workdir.name, 'enlol'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (out, err) = p.communicate() + self.assertEqual(out, b'') + self.assertIn(b'No lease found', err) + self.assertNotEqual(p.returncode, 0) + + def test_ip_leases_no_nm_lease(self): + os.environ['NETPLAN_GENERATE_PATH'] = os.path.join(rootdir, 'generate') + c = os.path.join(self.workdir.name, 'etc', 'netplan') + os.makedirs(c) + with open(os.path.join(c, 'a.yaml'), 'w') as f: + # match against loopback so as to successfully get a predictable + # ifindex + f.write('''network: + version: 2 + renderer: NetworkManager + ethernets: + enlol: + match: + name: lo + dhcp4: yes +''') + p = subprocess.Popen(exe_cli + + ['ip', 'leases', '--root-dir', self.workdir.name, 'enlol'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (out, err) = p.communicate() + self.assertEqual(out, b'') + self.assertIn(b'No lease found', err) + self.assertNotEqual(p.returncode, 0) + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/dbus/__init__.py b/tests/dbus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/dbus/test_dbus.py b/tests/dbus/test_dbus.py new file mode 100644 index 0000000..87abf22 --- /dev/null +++ b/tests/dbus/test_dbus.py @@ -0,0 +1,762 @@ +# +# Copyright (C) 2019-2020 Canonical, Ltd. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import shutil +import subprocess +import tempfile +import unittest +import time + +from tests.test_utils import MockCmd + +rootdir = os.path.dirname(os.path.dirname( + os.path.dirname(os.path.abspath(__file__)))) +exe_cli = [os.path.join(rootdir, 'src', 'netplan.script')] +if shutil.which('python3-coverage'): + exe_cli = ['python3-coverage', 'run', '--append', '--'] + exe_cli + +# Make sure we can import our development netplan. +os.environ.update({'PYTHONPATH': '.'}) +NETPLAN_DBUS_CMD = os.path.join(os.path.dirname(__file__), "..", "..", "netplan-dbus") + + +class TestNetplanDBus(unittest.TestCase): + + def setUp(self): + self.tmp = tempfile.mkdtemp() + os.makedirs(os.path.join(self.tmp, "etc", "netplan"), 0o700) + os.makedirs(os.path.join(self.tmp, "lib", "netplan"), 0o700) + os.makedirs(os.path.join(self.tmp, "run", "netplan"), 0o700) + # Create main test YAML in /etc/netplan/ + test_file = os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml') + with open(test_file, 'w') as f: + f.write("""network: + version: 2 + ethernets: + eth0: + dhcp4: true""") + self.addCleanup(shutil.rmtree, self.tmp) + self.mock_netplan_cmd = MockCmd("netplan") + self._create_mock_system_bus() + self._run_netplan_dbus_on_mock_bus() + self._mock_snap_env() + self.mock_busctl_cmd = MockCmd("busctl") + + def _mock_snap_env(self): + os.environ["SNAP"] = "test-netplan-apply-snapd" + + def _create_mock_system_bus(self): + env = {} + output = subprocess.check_output(["dbus-launch"], env={}) + for s in output.decode("utf-8").split("\n"): + if s == "": + continue + k, v = s.split("=", 1) + env[k] = v + # override system bus with the fake one + os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = env["DBUS_SESSION_BUS_ADDRESS"] + self.addCleanup(os.kill, int(env["DBUS_SESSION_BUS_PID"]), 15) + + def _run_netplan_dbus_on_mock_bus(self): + # run netplan-dbus in a fake system bus + os.environ["DBUS_TEST_NETPLAN_CMD"] = self.mock_netplan_cmd.path + os.environ["DBUS_TEST_NETPLAN_ROOT"] = self.tmp + p = subprocess.Popen(NETPLAN_DBUS_CMD, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + time.sleep(1) # Give some time for our dbus daemon to be ready + self.addCleanup(self._cleanup_netplan_dbus, p) + + def _cleanup_netplan_dbus(self, p): + p.terminate() + p.wait() + # netplan-dbus does not produce output + self.assertEqual(p.stdout.read(), b"") + self.assertEqual(p.stderr.read(), b"") + + def _check_dbus_error(self, cmd, returncode=1): + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + p.wait() + self.assertEqual(p.returncode, returncode) + self.assertEqual(p.stdout.read().decode("utf-8"), "") + return p.stderr.read().decode("utf-8") + + def _new_config_object(self): + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan", + "io.netplan.Netplan", + "Config", + ] + # Create new config object / config state + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertIn(b'o "/io/netplan/Netplan/config/', out) + cid = out.decode('utf-8').split('/')[-1].replace('"\n', '') + # Verify that the state folders were created in /tmp + tmpdir = '/tmp/netplan-config-{}'.format(cid) + self.assertTrue(os.path.isdir(tmpdir)) + self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'etc', 'netplan'))) + self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'run', 'netplan'))) + self.assertTrue(os.path.isdir(os.path.join(tmpdir, 'lib', 'netplan'))) + # Return random config ID + return cid + + def test_netplan_apply_in_snap_uses_dbus(self): + p = subprocess.Popen( + exe_cli + ["apply"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.assertEqual(p.stdout.read(), b"") + self.assertEqual(p.stderr.read(), b"") + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "apply"], + ]) + + def test_netplan_apply_in_snap_calls_busctl(self): + newenv = os.environ.copy() + busctlDir = os.path.dirname(self.mock_busctl_cmd.path) + newenv["PATH"] = busctlDir+":"+os.environ["PATH"] + p = subprocess.Popen( + exe_cli + ["apply"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=newenv) + p.wait(10) + self.assertEqual(p.stdout.read(), b"") + self.assertEqual(p.stderr.read(), b"") + self.assertEquals(self.mock_busctl_cmd.calls(), [ + ["busctl", "call", "--quiet", "--system", + "io.netplan.Netplan", # the service + "/io/netplan/Netplan", # the object + "io.netplan.Netplan", # the interface + "Apply", # the method + ], + ]) + + def test_netplan_apply_in_snap_calls_busctl_ret130(self): + newenv = os.environ.copy() + busctlDir = os.path.dirname(self.mock_busctl_cmd.path) + newenv["PATH"] = busctlDir+":"+os.environ["PATH"] + self.mock_busctl_cmd.set_returncode(130) + p = subprocess.Popen( + exe_cli + ["apply"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=newenv) + p.wait(10) + # exit_on_error is True by default, so we check the returncode directly + self.assertEqual(p.returncode, 130) + + def test_netplan_apply_in_snap_calls_busctl_err(self): + newenv = os.environ.copy() + busctlDir = os.path.dirname(self.mock_busctl_cmd.path) + newenv["PATH"] = busctlDir+":"+os.environ["PATH"] + self.mock_busctl_cmd.set_returncode(1) + p = subprocess.Popen( + exe_cli + ["apply"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=newenv) + p.wait(10) + # exit_on_error is True by default, so we check the returncode directly + self.assertEqual(p.returncode, 1) + + def test_netplan_generate_in_snap_calls_busctl(self): + newenv = os.environ.copy() + busctlDir = os.path.dirname(self.mock_busctl_cmd.path) + newenv["PATH"] = busctlDir+":"+os.environ["PATH"] + p = subprocess.Popen( + exe_cli + ["generate"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=newenv) + p.wait(10) + self.assertEqual(p.stdout.read(), b"") + self.assertEqual(p.stderr.read(), b"") + self.assertEquals(self.mock_busctl_cmd.calls(), [ + ["busctl", "call", "--quiet", "--system", + "io.netplan.Netplan", # the service + "/io/netplan/Netplan", # the object + "io.netplan.Netplan", # the interface + "Generate", # the method + ], + ]) + + def test_netplan_generate_in_snap_calls_busctl_ret130(self): + newenv = os.environ.copy() + busctlDir = os.path.dirname(self.mock_busctl_cmd.path) + newenv["PATH"] = busctlDir+":"+os.environ["PATH"] + self.mock_busctl_cmd.set_returncode(130) + p = subprocess.Popen( + exe_cli + ["generate"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=newenv) + p.wait(10) + self.assertIn(b"PermissionError: failed to communicate with dbus service", p.stderr.read()) + + def test_netplan_generate_in_snap_calls_busctl_err(self): + newenv = os.environ.copy() + busctlDir = os.path.dirname(self.mock_busctl_cmd.path) + newenv["PATH"] = busctlDir+":"+os.environ["PATH"] + self.mock_busctl_cmd.set_returncode(1) + p = subprocess.Popen( + exe_cli + ["generate"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=newenv) + p.wait(10) + self.assertIn(b"RuntimeError: failed to communicate with dbus service: error 1", p.stderr.read()) + + def test_netplan_dbus_noroot(self): + # Process should fail instantly, if not: kill it after 5 sec + r = subprocess.run(NETPLAN_DBUS_CMD, timeout=5, capture_output=True) + self.assertEquals(r.returncode, 1) + self.assertIn(b'Failed to acquire service name', r.stderr) + + def test_netplan_dbus_happy(self): + BUSCTL_NETPLAN_APPLY = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan", + "io.netplan.Netplan", + "Apply", + ] + output = subprocess.check_output(BUSCTL_NETPLAN_APPLY) + self.assertEqual(output.decode("utf-8"), "b true\n") + # one call to netplan apply in total + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "apply"], + ]) + + # and again! + output = subprocess.check_output(BUSCTL_NETPLAN_APPLY) + self.assertEqual(output.decode("utf-8"), "b true\n") + # and another call to netplan apply + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "apply"], + ["netplan", "apply"], + ]) + + def test_netplan_dbus_generate(self): + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan", + "io.netplan.Netplan", + "Generate", + ] + output = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(output.decode("utf-8"), "b true\n") + # one call to netplan apply in total + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "generate"], + ]) + + def test_netplan_dbus_info(self): + BUSCTL_NETPLAN_INFO = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan", + "io.netplan.Netplan", + "Info", + ] + output = subprocess.check_output(BUSCTL_NETPLAN_INFO) + self.assertIn("Features", output.decode("utf-8")) + + def test_netplan_dbus_config(self): + # Create test YAML + test_file_lib = os.path.join(self.tmp, 'lib', 'netplan', 'lib_test.yaml') + with open(test_file_lib, 'w') as f: + f.write('TESTING-lib') + test_file_run = os.path.join(self.tmp, 'run', 'netplan', 'run_test.yaml') + with open(test_file_run, 'w') as f: + f.write('TESTING-run') + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'lib_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'run_test.yaml'))) + + cid = self._new_config_object() + tmpdir = '/tmp/netplan-config-{}'.format(cid) + self.addClassCleanup(shutil.rmtree, tmpdir) + + # Verify the object path has been created, by calling .Config.Get() on that object + # it would throw an error if it does not exist + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Get", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD, universal_newlines=True) + self.assertIn(r's ""', out) # No output as 'netplan get' is actually mocked + self.assertEquals(self.mock_netplan_cmd.calls(), [[ + "netplan", "get", "all", "--root-dir={}".format(tmpdir) + ]]) + + # Verify all *.yaml files have been copied + self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'etc', 'netplan', 'main_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'lib', 'netplan', 'lib_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'run', 'netplan', 'run_test.yaml'))) + + def test_netplan_dbus_no_such_command(self): + err = self._check_dbus_error([ + "busctl", "call", + "io.netplan.Netplan", + "/io/netplan/Netplan", + "io.netplan.Netplan", + "NoSuchCommand" + ]) + self.assertIn("Unknown method", err) + + def test_netplan_dbus_config_set(self): + cid = self._new_config_object() + tmpdir = '/tmp/netplan-config-{}'.format(cid) + self.addCleanup(shutil.rmtree, tmpdir) + + # Verify .Config.Set() on the config object + # No actual YAML file will be created, as the netplan command is mocked + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth42.dhcp6=true", "", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + print(self.mock_netplan_cmd.calls(), flush=True) + self.assertEquals(self.mock_netplan_cmd.calls(), [[ + "netplan", "set", "ethernets.eth42.dhcp6=true", + "--root-dir={}".format(tmpdir) + ]]) + + def test_netplan_dbus_config_get(self): + cid = self._new_config_object() + tmpdir = '/tmp/netplan-config-{}'.format(cid) + self.addCleanup(shutil.rmtree, tmpdir) + + # Verify .Config.Get() on the config object + self.mock_netplan_cmd.set_output("network:\n eth42:\n dhcp6: true") + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Get", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD, universal_newlines=True) + self.assertIn(r's "network:\n eth42:\n dhcp6: true\n"', out) + self.assertEquals(self.mock_netplan_cmd.calls(), [[ + "netplan", "get", "all", "--root-dir={}".format(tmpdir) + ]]) + + def test_netplan_dbus_config_cancel(self): + cid = self._new_config_object() + tmpdir = '/tmp/netplan-config-{}'.format(cid) + + # Verify .Config.Cancel() teardown of the config object and state dirs + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Cancel", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + + time.sleep(1) # Give some time for 'Cancel' to clean up + self.assertFalse(os.path.isdir(tmpdir)) + + # Verify the object is gone from the bus + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) + self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) + + def test_netplan_dbus_config_apply(self): + cid = self._new_config_object() + tmpdir = '/tmp/netplan-config-{}'.format(cid) + with open(os.path.join(tmpdir, 'etc', 'netplan', 'apply_test.yaml'), 'w') as f: + f.write('TESTING-apply') + with open(os.path.join(tmpdir, 'lib', 'netplan', 'apply_test.yaml'), 'w') as f: + f.write('TESTING-apply') + with open(os.path.join(tmpdir, 'run', 'netplan', 'apply_test.yaml'), 'w') as f: + f.write('TESTING-apply') + + # Verify .Config.Apply() teardown of the config object and state dirs + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Apply", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "apply"]]) + time.sleep(1) # Give some time for 'Apply' to clean up + self.assertFalse(os.path.isdir(tmpdir)) + + # Verify the new YAML files were copied over + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'apply_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'apply_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'apply_test.yaml'))) + + # Verify the object is gone from the bus + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) + self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) + + def test_netplan_dbus_config_try_cancel(self): + # self-terminate after 30 dsec = 3 sec, if not cancelled before + self.mock_netplan_cmd.set_timeout(30) + cid = self._new_config_object() + tmpdir = '/tmp/netplan-config-{}'.format(cid) + backup = '/tmp/netplan-config-BACKUP' + with open(os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'), 'w') as f: + f.write('TESTING-try') + with open(os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'), 'w') as f: + f.write('TESTING-try') + with open(os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'), 'w') as f: + f.write('TESTING-try') + + # Verify .Config.Try() setup of the config object and state dirs + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Try", "u", "3", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + + # Verify the temp state still exists + self.assertTrue(os.path.isdir(tmpdir)) + self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'))) + + # Verify the backup has been created + self.assertTrue(os.path.isdir(backup)) + self.assertTrue(os.path.isfile(os.path.join(backup, 'etc', 'netplan', 'main_test.yaml'))) + + # Verify the new YAML files were copied over + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) + + BUSCTL_NETPLAN_CMD2 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Cancel", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) + self.assertEqual(b'b true\n', out) + time.sleep(1) # Give some time for 'Cancel' to clean up + + # Verify the backup andconfig state dir are gone + self.assertFalse(os.path.isdir(backup)) + self.assertFalse(os.path.isdir(tmpdir)) + + # Verify the backup has been restored + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) + self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) + self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) + self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) + + # Verify the config object is gone from the bus + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) + self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) + + # Verify 'netplan try' has been called + self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=3"]]) + + def test_netplan_dbus_config_try_cb(self): + self.mock_netplan_cmd.set_timeout(1) # actually self-terminate after 0.1 sec + cid = self._new_config_object() + tmpdir = '/tmp/netplan-config-{}'.format(cid) + backup = '/tmp/netplan-config-BACKUP' + with open(os.path.join(tmpdir, 'etc', 'netplan', 'try_test.yaml'), 'w') as f: + f.write('TESTING-try') + with open(os.path.join(tmpdir, 'lib', 'netplan', 'try_test.yaml'), 'w') as f: + f.write('TESTING-try') + with open(os.path.join(tmpdir, 'run', 'netplan', 'try_test.yaml'), 'w') as f: + f.write('TESTING-try') + + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Try", "u", "1", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + time.sleep(1.5) # Give some time for the timeout to happen + + # Verify the backup andconfig state dir are gone + self.assertFalse(os.path.isdir(backup)) + self.assertFalse(os.path.isdir(tmpdir)) + + # Verify the backup has been restored + self.assertTrue(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'main_test.yaml'))) + self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'etc', 'netplan', 'try_test.yaml'))) + self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'run', 'netplan', 'try_test.yaml'))) + self.assertFalse(os.path.isfile(os.path.join(self.tmp, 'lib', 'netplan', 'try_test.yaml'))) + + # Verify the config object is gone from the bus + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD) + self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) + + # Verify 'netplan try' has been called + self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=1"]]) + + def test_netplan_dbus_config_try_apply(self): + self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec + cid = self._new_config_object() + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Try", "u", "3", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + + BUSCTL_NETPLAN_CMD2 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan", + "io.netplan.Netplan", + "Apply", + ] + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) + self.assertIn('Another \'netplan try\' process is already running', err) + + def test_netplan_dbus_config_try_config_try(self): + self.mock_netplan_cmd.set_timeout(50) # 50 dsec = 5 sec + cid = self._new_config_object() + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Try", "u", "3", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + + cid2 = self._new_config_object() + BUSCTL_NETPLAN_CMD2 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid2), + "io.netplan.Netplan.Config", + "Try", "u", "5", + ] + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) + self.assertIn('Another Try() is currently in progress: PID ', err) + + def test_netplan_dbus_config_set_invalidate(self): + self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec + cid = self._new_config_object() + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + # Calling Set() on the same config object still works + BUSCTL_NETPLAN_CMD1 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth0.dhcp4=yes", "70-snapd", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD1) + self.assertEqual(b'b true\n', out) + + cid2 = self._new_config_object() + # Calling Set() on another config object fails + BUSCTL_NETPLAN_CMD2 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid2), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", + ] + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) + self.assertIn('This config was invalidated by another config object', err) + # Calling Try() on another config object fails + BUSCTL_NETPLAN_CMD3 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid2), + "io.netplan.Netplan.Config", + "Try", "u", "3", + ] + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD3) + self.assertIn('This config was invalidated by another config object', err) + # Calling Apply() on another config object fails + BUSCTL_NETPLAN_CMD4 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid2), + "io.netplan.Netplan.Config", + "Apply", + ] + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD4) + self.assertIn('This config was invalidated by another config object', err) + + # Calling Apply() on the same config object still works + BUSCTL_NETPLAN_CMD5 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Apply", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD5) + self.assertEqual(b'b true\n', out) + + # Verify that Set()/Apply() was only called by one config object + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", + "--root-dir=/tmp/netplan-config-{}".format(cid)], + ["netplan", "set", "ethernets.eth0.dhcp4=yes", "--origin-hint=70-snapd", + "--root-dir=/tmp/netplan-config-{}".format(cid)], + ["netplan", "apply"] + ]) + + # Now it works again + cid3 = self._new_config_object() + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid3), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid3), + "io.netplan.Netplan.Config", + "Apply", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + + def test_netplan_dbus_config_set_uninvalidate(self): + self.mock_netplan_cmd.set_timeout(2) + cid = self._new_config_object() + cid2 = self._new_config_object() + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + + # Calling Set() on another config object fails + BUSCTL_NETPLAN_CMD2 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid2), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", + ] + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) + self.assertIn('This config was invalidated by another config object', err) + + # Calling Cancel() clears the dirty state + BUSCTL_NETPLAN_CMD3 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Cancel", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD3) + self.assertEqual(b'b true\n', out) + + # Calling Set() on the other config object works now + out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) + self.assertEqual(b'b true\n', out) + + # Verify the call stack + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", + "--root-dir=/tmp/netplan-config-{}".format(cid)], + ["netplan", "set", "ethernets.eth0.dhcp4=false", "--origin-hint=70-snapd", + "--root-dir=/tmp/netplan-config-{}".format(cid2)] + ]) + + def test_netplan_dbus_config_set_uninvalidate_timeout(self): + self.mock_netplan_cmd.set_timeout(1) # actually self-terminate process after 0.1 sec + cid = self._new_config_object() + cid2 = self._new_config_object() + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth0.dhcp4=true", "70-snapd", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + + BUSCTL_NETPLAN_CMD1 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", + "Try", "u", "1", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD1) + self.assertEqual(b'b true\n', out) + + # Calling Set() on another config object fails + BUSCTL_NETPLAN_CMD2 = [ + "busctl", "call", "--system", + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid2), + "io.netplan.Netplan.Config", + "Set", "ss", "ethernets.eth0.dhcp4=false", "70-snapd", + ] + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD2) + self.assertIn('This config was invalidated by another config object', err) + + time.sleep(1.5) # Wait for the child process to self-terminate + + # Calling Set() on the other config object works now + out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) + self.assertEqual(b'b true\n', out) + + # Verify the call stack + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd", + "--root-dir=/tmp/netplan-config-{}".format(cid)], + ["netplan", "try", "--timeout=1"], + ["netplan", "set", "ethernets.eth0.dhcp4=false", "--origin-hint=70-snapd", + "--root-dir=/tmp/netplan-config-{}".format(cid2)] + ]) diff --git a/tests/generator/__init__.py b/tests/generator/__init__.py new file mode 100644 index 0000000..81eadaa --- /dev/null +++ b/tests/generator/__init__.py @@ -0,0 +1,17 @@ +# +# __init__ for generator tests. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/tests/generator/base.py b/tests/generator/base.py new file mode 100644 index 0000000..d72974b --- /dev/null +++ b/tests/generator/base.py @@ -0,0 +1,446 @@ +# +# Blackbox tests of netplan generate that verify that the generated +# configuration files look as expected. These are run during "make check" and +# don't touch the system configuration at all. +# +# Copyright (C) 2016-2021 Canonical, Ltd. +# Author: Martin Pitt +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import random +import glob +import stat +import string +import tempfile +import subprocess +import unittest +import ctypes +import ctypes.util +import yaml +import difflib + +exe_generate = os.path.join(os.path.dirname(os.path.dirname( + os.path.dirname(os.path.abspath(__file__)))), 'generate') + +# make sure we point to libnetplan properly. +os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) + +# make sure we fail on criticals +os.environ['G_DEBUG'] = 'fatal-criticals' + +lib = ctypes.CDLL(ctypes.util.find_library('netplan')) + +# common patterns for expected output +ND_EMPTY = '[Match]\nName=%s\n\n[Network]\nLinkLocalAddressing=%s\nConfigureWithoutCarrier=yes\n' +ND_WITHIP = '[Match]\nName=%s\n\n[Network]\nLinkLocalAddressing=ipv6\nAddress=%s\nConfigureWithoutCarrier=yes\n' +ND_WIFI_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=600\nUseMTU=true\n' +ND_DHCP = '[Match]\nName=%s\n\n[Network]\nDHCP=%s\nLinkLocalAddressing=ipv6%s\n\n[DHCP]\nRouteMetric=100\nUseMTU=%s\n' +ND_DHCP4 = ND_DHCP % ('%s', 'ipv4', '', 'true') +ND_DHCP4_NOMTU = ND_DHCP % ('%s', 'ipv4', '', 'false') +ND_DHCP6 = ND_DHCP % ('%s', 'ipv6', '', 'true') +ND_DHCP6_NOMTU = ND_DHCP % ('%s', 'ipv6', '', 'false') +ND_DHCP6_WOCARRIER = ND_DHCP % ('%s', 'ipv6', '\nConfigureWithoutCarrier=yes', 'true') +ND_DHCPYES = ND_DHCP % ('%s', 'yes', '', 'true') +ND_DHCPYES_NOMTU = ND_DHCP % ('%s', 'yes', '', 'false') +_OVS_BASE = '[Unit]\nDescription=OpenVSwitch configuration for %(iface)s\nDefaultDependencies=no\n\ +Wants=ovsdb-server.service\nAfter=ovsdb-server.service\n' +OVS_PHYSICAL = _OVS_BASE + 'Requires=sys-subsystem-net-devices-%(iface)s.device\nAfter=sys-subsystem-net-devices-%(iface)s\ +.device\nAfter=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n%(extra)s' +OVS_VIRTUAL = _OVS_BASE + 'After=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n%(extra)s' +OVS_BR_DEFAULT = 'ExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan=true\nExecStart=/usr/bin/ovs-vsctl \ +set-fail-mode %(iface)s standalone\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan/global/set-fail-mode=\ +standalone\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s mcast_snooping_enable=false\nExecStart=/usr/bin/ovs-vsctl set \ +Bridge %(iface)s external-ids:netplan/mcast_snooping_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s \ +rstp_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan/rstp_enable=false\n' +OVS_BR_EMPTY = _OVS_BASE + 'After=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n\n[Service]\n\ +Type=oneshot\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT +OVS_CLEANUP = _OVS_BASE + 'ConditionFileIsExecutable=/usr/bin/ovs-vsctl\nBefore=network.target\nWants=network.target\n\n\ +[Service]\nType=oneshot\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' +UDEV_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", ATTR{address}=="%s", NAME="%s"\n' +UDEV_NO_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", NAME="%s"\n' +UDEV_SRIOV_RULE = 'ACTION=="add", SUBSYSTEM=="net", ATTRS{sriov_totalvfs}=="?*", RUN+="/usr/sbin/netplan apply --sriov-only"\n' +ND_WITHIPGW = '[Match]\nName=%s\n\n[Network]\nLinkLocalAddressing=ipv6\nAddress=%s\nAddress=%s\nGateway=%s\n\ +ConfigureWithoutCarrier=yes\n' +NM_WG = '[connection]\nid=netplan-wg0\ntype=wireguard\ninterface-name=wg0\n\n[wireguard]\nprivate-key=%s\nlisten-port=%s\n%s\ +\n\n[ipv4]\nmethod=manual\naddress1=15.15.15.15/24\ngateway=20.20.20.21\n\n[ipv6]\nmethod=manual\naddress1=\ +2001:de:ad:be:ef:ca:fe:1/128\n' +ND_WG = '[NetDev]\nName=wg0\nKind=wireguard\n\n[WireGuard]\nPrivateKey%s\nListenPort=%s\n%s\n' +ND_VLAN = '[NetDev]\nName=%s\nKind=vlan\n\n[VLAN]\nId=%d\n' + + +class NetplanV2Normalizer(): + + def __init__(self): + self.YAML_FALSE = ['n', 'no', 'off', 'false'] + self.YAML_TRUE = ['y', 'yes', 'on', 'true'] + self.DEFAULT_STANZAS = [ + 'dhcp4-overrides: {}', # 2nd level default (containing defaults itself) + 'dhcp6-overrides: {}', # 2nd level default (containing defaults itself) + 'hidden: false', # access-point + 'on-link: false', # route + 'stp: true', # paramters + 'type: unicast', # route + 'version: 2', # global + ] + self.DEFAULT_NETDEF = { + 'dhcp4': self.YAML_FALSE, + 'dhcp6': self.YAML_FALSE, + 'dhcp-identifier': ['duid'], + 'hidden': self.YAML_FALSE, + } + self.DEFAULT_DHCP = { + 'send-hostname': self.YAML_TRUE, + 'use-dns': self.YAML_TRUE, + 'use-hostname': self.YAML_TRUE, + 'use-mtu': self.YAML_TRUE, + 'use-ntp': self.YAML_TRUE, + 'use-routes': self.YAML_TRUE, + } + + def _clear_mapping_defaults(self, keys, defaults, data): + potential_defaults = list(set(keys) & set(defaults.keys())) + for k in potential_defaults: + if any(map(str(data[k]).lower().__eq__, defaults[k])): + del data[k] + + def normalize_yaml_line(self, line): + '''Process formatted YAML line by line (one setting/key per line) + + Deleting default values and re-writing to default wording + ''' + kv = line.replace('"', '').replace('\'', '').split(':', 1) + if len(kv) != 2 or kv[1].isspace() or kv[1] == '': + return line # no normalization needed; no value given + + # normalize key + key = kv[0] + if 'gratuitious-arp' in key: # historically supported typo + kv[0] = key.replace('gratuitious-arp', 'gratuitous-arp') + + # normalize value + val = kv[1].strip() + if val in self.YAML_FALSE: + kv[1] = 'false' + elif val in self.YAML_TRUE: + kv[1] = 'true' + elif val == '5G': + kv[1] = '5GHz' + elif val == '2.4G': + kv[1] = '2.4GHz' + else: # no normalization needed or known + kv[1] = val + + return ': '.join(kv) + + def normalize_yaml_tree(self, data, full_key=''): + '''Walk the YAML dict/tree @data and sort its sequences in place + + Keeping track of the @full_key (path), e.g.: "network:ethernets:eth0:dhcp4" + And normalizing certain netplan special cases + ''' + if isinstance(data, list): + scalars_only = not any(list(map(lambda elem: (isinstance(elem, dict) or isinstance(elem, list)), data))) + # sort sequence alphabetically + if scalars_only: + data.sort() + # remove duplicates (if needed) + unique = set(data) + if len(data) > len(unique): + rm_idx = set() + last_idx = 0 + for elem in unique: + if data.count(elem) > 1: + idx = data.index(elem, last_idx) + rm_idx.add(idx) + last_idx = idx + for idx in rm_idx: + del data[idx] + elif isinstance(data, dict): + keys = data.keys() + # expand special short forms + if 'password' in keys and ':auth' not in full_key: + data['auth'] = {'key-management': 'psk', 'password': data['password']} + del data['password'] + elif 'auth' in keys and data['auth'] == {}: + data['auth'] = {'key-management': 'none'} + # remove default stanza ("link-local: [ ipv6 ]"") + elif 'link-local' in keys and data['link-local'] == ['ipv6']: + del data['link-local'] + # remove default stanza ("wakeonwlan: [ default ]") + elif 'wakeonwlan' in keys and data['wakeonwlan'] == ['default']: + del data['wakeonwlan'] + # remove explicit openvswitch stanzas, they might not always be + # defined in the original YAML (due to being implicit) + elif ('openvswitch' in keys and data['openvswitch'] == {} and + any(map(full_key.__contains__, [':bonds:', ':bridges:', ':vlans:']))): + del data['openvswitch'] + # remove default empty bond-parameters, those are not rendered by the YAML generator + elif 'parameters' in keys and data['parameters'] == {} and ':bonds:' in full_key: + del data['parameters'] + # remove default mode=infrastructore from wifi APs, keeping the SSID + elif 'mode' in keys and ':wifis:' in full_key and 'infrastructure' in data['mode']: + del data['mode'] + # ignore renderer: on other than global levels for now, as that + # information is currently not stored in the netdef data structure + elif ('renderer' in keys and len(full_key.split(':')) > 1 and + data['renderer'] in ['networkd', 'NetworkManager']): + del data['renderer'] + # remove default values from the dhcp4/6-overrides mappings + elif full_key.endswith(':dhcp4-overrides') or full_key.endswith(':dhcp6-overrides'): + self._clear_mapping_defaults(keys, self.DEFAULT_DHCP, data) + # remove default values from netdef/interface mappings + elif len(full_key.split(':')) == 3: # netdef level + self._clear_mapping_defaults(keys, self.DEFAULT_NETDEF, data) + + # continue to walk the dict + for key in data.keys(): + full_key_next = ':'.join([str(full_key), str(key)]) if full_key != '' else key + self.normalize_yaml_tree(data[key], full_key_next) + + def normalize_yaml(self, yaml_dict): + # 1st pass: normalize the YAML tree in place, sorting and removing some values + self.normalize_yaml_tree(yaml_dict) + # 2nd pass: sort the mapping keys and output a formatted yaml (one key per line) + formatted_yaml = yaml.dump(yaml_dict, sort_keys=True) + # 3rd pass: normalize the wording of certain keys/values per line + # and remove any line, containg only default values + output = [] + for line in formatted_yaml.splitlines(): + line = self.normalize_yaml_line(line) + if line.strip() in self.DEFAULT_STANZAS: + continue + output.append(line) + return output + + +class TestBase(unittest.TestCase): + + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + self.confdir = os.path.join(self.workdir.name, 'etc', 'netplan') + self.nm_enable_all_conf = os.path.join( + self.workdir.name, 'run', 'NetworkManager', 'conf.d', '10-globally-managed-devices.conf') + self.maxDiff = None + + def validate_generated_yaml(self, yaml_input): + '''Validate a list of YAML input files one by one. + + Go through the list @yaml_input one by one, parse the YAML and + re-generate the YAML output. Afterwards, normalize and compare the + original (and normalized) input with the generated (and normalized) + output. + ''' + output = '_generated_test_output.yaml' + output_path = os.path.join(self.confdir, output) + + for input in yaml_input: + lib.netplan_clear_netdefs() # clear previous netdefs + lib.netplan_parse_yaml(input.encode(), None) + lib.write_netplan_conf_full(output.encode(), self.workdir.name.encode()) + + input_yaml = None + output_yaml = None + + # Read input YAML file, as defined by the self.generate('...') method + with open(input, 'r') as orig: + input_yaml = yaml.safe_load(orig.read()) + # Consider 'network: {}' and 'network: {version: 2}' to be empty + if input_yaml is None or input_yaml == {'network': {}} or input_yaml == {'network': {'version': 2}}: + input_yaml = yaml.safe_load('') + + # Read output of the YAML generator (if any) + if os.path.isfile(output_path): + with open(output_path, 'r') as generated: + output_yaml = yaml.safe_load(generated.read()) + else: + output_yaml = yaml.safe_load('') + + # Normalize input and output YAML + netplan_normalizer = NetplanV2Normalizer() + input_lines = netplan_normalizer.normalize_yaml(input_yaml) + output_lines = netplan_normalizer.normalize_yaml(output_yaml) + + # Check if (normalized) input and (normalized) output are equal + yaml_files_differ = len(input_lines) != len(output_lines) + if not yaml_files_differ: # pragma: no cover (only execited in error case) + for i in range(len(input_lines)): + if input_lines[i] != output_lines[i]: + yaml_files_differ = True + break + if yaml_files_differ: # pragma: no cover (only execited in error case) + fromfile = 'original (%s)' % input + for line in difflib.unified_diff(input_lines, output_lines, fromfile, tofile='generated', lineterm=''): + print(line, flush=True) + self.fail('Re-generated YAML file does not match (adopt netplan.c YAML generator?)') + + # Cleanup the generated file and data structures + lib.netplan_clear_netdefs() + if os.path.isfile(output_path): + os.remove(output_path) + + def generate(self, yaml, expect_fail=False, extra_args=[], confs=None, skip_generated_yaml_validation=False): + '''Call generate with given YAML string as configuration + + Return stderr output. + ''' + yaml_input = [] + conf = os.path.join(self.confdir, 'a.yaml') + os.makedirs(os.path.dirname(conf), exist_ok=True) + if yaml is not None: + with open(conf, 'w') as f: + f.write(yaml) + yaml_input.append(conf) + if confs: + for f, contents in confs.items(): + path = os.path.join(self.confdir, f + '.yaml') + with open(path, 'w') as f: + f.write(contents) + yaml_input.append(path) + + argv = [exe_generate, '--root-dir', self.workdir.name] + extra_args + if 'TEST_SHELL' in os.environ: # pragma nocover + print('Test is about to run:\n%s' % ' '.join(argv)) + subprocess.call(['bash', '-i'], cwd=self.workdir.name) + + p = subprocess.Popen(argv, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True) + (out, err) = p.communicate() + if expect_fail: + self.assertGreater(p.returncode, 0) + else: + self.assertEqual(p.returncode, 0, err) + self.assertEqual(out, '') + if not expect_fail and not skip_generated_yaml_validation: + yaml_input = list(set(yaml_input + extra_args)) + yaml_input.sort() + self.validate_generated_yaml(yaml_input) + return err + + def eth_name(self): + """Return a link name. + + Use when you need a link name for a test but don't want to + encode a made up name in the test. + """ + return 'eth' + ''.join(random.sample(string.ascii_letters + string.digits, k=4)) + + def assert_networkd(self, file_contents_map): + networkd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'network') + if not file_contents_map: + self.assertFalse(os.path.exists(networkd_dir)) + return + + self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'}) + self.assertEqual(set(os.listdir(networkd_dir)), + {'10-netplan-' + f for f in file_contents_map}) + for fname, contents in file_contents_map.items(): + with open(os.path.join(networkd_dir, '10-netplan-' + fname)) as f: + self.assertEqual(f.read(), contents) + + def assert_additional_udev(self, file_contents_map): + udev_dir = os.path.join(self.workdir.name, 'run', 'udev', 'rules.d') + for fname, contents in file_contents_map.items(): + with open(os.path.join(udev_dir, fname)) as f: + self.assertEqual(f.read(), contents) + + def assert_networkd_udev(self, file_contents_map): + udev_dir = os.path.join(self.workdir.name, 'run', 'udev', 'rules.d') + if not file_contents_map: + # it can either not exist, or can only contain 90-netplan.rules + self.assertTrue((not os.path.exists(udev_dir)) or + (os.listdir(udev_dir) == ['90-netplan.rules'])) + return + + self.assertEqual(set(os.listdir(udev_dir)) - set(['90-netplan.rules']), + {'99-netplan-' + f for f in file_contents_map}) + for fname, contents in file_contents_map.items(): + with open(os.path.join(udev_dir, '99-netplan-' + fname)) as f: + self.assertEqual(f.read(), contents) + + def get_network_config_for_link(self, link_name): + """Return the content of the .network file for `link_name`.""" + networkd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'network') + with open(os.path.join(networkd_dir, '10-netplan-{}.network'.format(link_name))) as f: + return f.read() + + def get_optional_addresses(self, eth_name): + config = self.get_network_config_for_link(eth_name) + r = set() + prefix = "OptionalAddresses=" + for line in config.splitlines(): + if line.startswith(prefix): + r.add(line[len(prefix):]) + return r + + def assert_nm(self, connections_map=None, conf=None): + # check config + conf_path = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'conf.d', 'netplan.conf') + if conf: + with open(conf_path) as f: + self.assertEqual(f.read(), conf) + else: + if os.path.exists(conf_path): + with open(conf_path) as f: # pragma: nocover + self.fail('unexpected %s:\n%s' % (conf_path, f.read())) + + # check connections + con_dir = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'system-connections') + if connections_map: + self.assertEqual(set(os.listdir(con_dir)), + set(['netplan-' + n.split('.nmconnection')[0] + '.nmconnection' for n in connections_map])) + for fname, contents in connections_map.items(): + extension = '' + if '.nmconnection' not in fname: + extension = '.nmconnection' + with open(os.path.join(con_dir, 'netplan-' + fname + extension)) as f: + self.assertEqual(f.read(), contents) + # NM connection files might contain secrets + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + else: + if os.path.exists(con_dir): + self.assertEqual(os.listdir(con_dir), []) + + def assert_nm_udev(self, contents): + rule_path = os.path.join(self.workdir.name, 'run/udev/rules.d/90-netplan.rules') + if contents is None: + self.assertFalse(os.path.exists(rule_path)) + return + with open(rule_path) as f: + self.assertEqual(f.read(), contents) + + def assert_ovs(self, file_contents_map): + systemd_dir = os.path.join(self.workdir.name, 'run', 'systemd', 'system') + if not file_contents_map: + # in this case we assume no OVS configuration should be present + self.assertFalse(glob.glob(os.path.join(systemd_dir, '*netplan-ovs-*.service'))) + return + + self.assertEqual(set(os.listdir(self.workdir.name)) - {'lib'}, {'etc', 'run'}) + ovs_systemd_dir = set(os.listdir(systemd_dir)) + ovs_systemd_dir.remove('systemd-networkd.service.wants') + self.assertEqual(ovs_systemd_dir, {'netplan-ovs-' + f for f in file_contents_map}) + for fname, contents in file_contents_map.items(): + fname = 'netplan-ovs-' + fname + with open(os.path.join(systemd_dir, fname)) as f: + self.assertEqual(f.read(), contents) + if fname.endswith('.service'): + link_path = os.path.join( + systemd_dir, 'systemd-networkd.service.wants', fname) + self.assertTrue(os.path.islink(link_path)) + link_target = os.readlink(link_path) + self.assertEqual(link_target, + os.path.join( + '/', 'run', 'systemd', 'system', fname)) diff --git a/tests/generator/test_args.py b/tests/generator/test_args.py new file mode 100644 index 0000000..250d317 --- /dev/null +++ b/tests/generator/test_args.py @@ -0,0 +1,184 @@ +# +# Command-line arguments handling tests for generator +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import subprocess + +from .base import TestBase, exe_generate, OVS_CLEANUP + + +class TestConfigArgs(TestBase): + '''Config file argument handling''' + + def test_no_files(self): + subprocess.check_call([exe_generate, '--root-dir', self.workdir.name]) + self.assertEqual(os.listdir(self.workdir.name), ['run']) + self.assert_nm_udev(None) + + def test_no_configs(self): + self.generate('network:\n version: 2') + # should not write any files + self.assertCountEqual(os.listdir(self.workdir.name), ['etc', 'run']) + self.assert_networkd(None) + self.assert_networkd_udev(None) + self.assert_nm(None) + self.assert_nm_udev(None) + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + + def test_empty_config(self): + self.generate('') + # should not write any files + self.assertCountEqual(os.listdir(self.workdir.name), ['etc', 'run']) + self.assert_networkd(None) + self.assert_networkd_udev(None) + self.assert_nm(None) + self.assert_nm_udev(None) + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + + def test_file_args(self): + conf = os.path.join(self.workdir.name, 'config') + with open(conf, 'w') as f: + f.write('network: {}') + # when specifying custom files, it should ignore the global config + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''', extra_args=[conf]) + # There is one systemd service unit 'netplan-ovs-cleanup.service' in /run, + # which will always be created + self.assertEqual(set(os.listdir(self.workdir.name)), {'config', 'etc', 'run'}) + self.assert_networkd(None) + self.assert_networkd_udev(None) + self.assert_nm(None) + self.assert_nm_udev(None) + + def test_file_args_notfound(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''', expect_fail=True, extra_args=['/non/existing/config']) + self.assertEqual(err, 'Cannot open /non/existing/config: No such file or directory\n') + self.assertEqual(os.listdir(self.workdir.name), ['etc']) + + def test_help(self): + conf = os.path.join(self.workdir.name, 'etc', 'netplan', 'a.yaml') + os.makedirs(os.path.dirname(conf)) + with open(conf, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''') + + p = subprocess.Popen([exe_generate, '--root-dir', self.workdir.name, '--help'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + (out, err) = p.communicate() + self.assertEqual(err, '') + self.assertEqual(p.returncode, 0) + self.assertIn('Usage:', out) + self.assertEqual(os.listdir(self.workdir.name), ['etc']) + + def test_unknown_cli_args(self): + p = subprocess.Popen([exe_generate, '--foo'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + (out, err) = p.communicate() + self.assertIn('nknown option --foo', err) + self.assertNotEqual(p.returncode, 0) + + def test_output_mkdir_error(self): + conf = os.path.join(self.workdir.name, 'config') + with open(conf, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''') + err = self.generate('', extra_args=['--root-dir', '/proc/foo', conf], expect_fail=True) + # can be /proc/foor/run/systemd/{network,system} + self.assertIn('cannot create directory /proc/foo/run/systemd/', err) + + def test_systemd_generator(self): + conf = os.path.join(self.confdir, 'a.yaml') + os.makedirs(os.path.dirname(conf)) + with open(conf, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0: + dhcp4: true''') + outdir = os.path.join(self.workdir.name, 'out') + os.mkdir(outdir) + + generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') + os.makedirs(os.path.dirname(generator)) + os.symlink(exe_generate, generator) + + subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) + n = os.path.join(self.workdir.name, 'run', 'systemd', 'network', '10-netplan-eth0.network') + self.assertTrue(os.path.exists(n)) + os.unlink(n) + + # should auto-enable networkd and -wait-online + self.assertTrue(os.path.islink(os.path.join( + outdir, 'multi-user.target.wants', 'systemd-networkd.service'))) + self.assertTrue(os.path.islink(os.path.join( + outdir, 'network-online.target.wants', 'systemd-networkd-wait-online.service'))) + + # should be a no-op the second time while the stamp exists + out = subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir], + stderr=subprocess.STDOUT) + self.assertFalse(os.path.exists(n)) + self.assertIn(b'netplan generate already ran', out) + + # after removing the stamp it generates again, and not trip over the + # existing enablement symlink + os.unlink(os.path.join(outdir, 'netplan.stamp')) + subprocess.check_output([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) + self.assertTrue(os.path.exists(n)) + + def test_systemd_generator_noconf(self): + outdir = os.path.join(self.workdir.name, 'out') + os.mkdir(outdir) + + generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') + os.makedirs(os.path.dirname(generator)) + os.symlink(exe_generate, generator) + + subprocess.check_call([generator, '--root-dir', self.workdir.name, outdir, outdir, outdir]) + # no enablement symlink here + self.assertEqual(os.listdir(outdir), ['netplan.stamp']) + + def test_systemd_generator_badcall(self): + outdir = os.path.join(self.workdir.name, 'out') + os.mkdir(outdir) + + generator = os.path.join(self.workdir.name, 'systemd', 'system-generators', 'netplan') + os.makedirs(os.path.dirname(generator)) + os.symlink(exe_generate, generator) + + try: + subprocess.check_output([generator, '--root-dir', self.workdir.name], + stderr=subprocess.STDOUT) + self.fail("direct systemd generator call is expected to fail, but succeeded.") # pragma: nocover + except subprocess.CalledProcessError as e: + self.assertEqual(e.returncode, 1) + self.assertIn(b'can not be called directly', e.output) diff --git a/tests/generator/test_auth.py b/tests/generator/test_auth.py new file mode 100644 index 0000000..7d9ff8f --- /dev/null +++ b/tests/generator/test_auth.py @@ -0,0 +1,555 @@ +# +# Tests for network authentication config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import stat + +from .base import TestBase, ND_DHCP4, ND_WIFI_DHCP4 + + +class TestNetworkd(TestBase): + + def test_auth_wifi_detailed(self): + self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "s0s3kr1t" + "Luke's Home": + auth: + key-management: psk + password: "4lsos3kr1t" + "BobsHome": + password: "e03ce667c87bc81ca968d9120ca37f89eb09aec3c55b80386e5d772efd6b926e" + "BillsHome": + auth: + key-management: psk + password: "db3b0acf5653aeaddd5fe034fb9f07175b2864f847b005aaa2f09182d9411b04" + workplace: + auth: + key-management: eap + method: ttls + anonymous-identity: "@internal.example.com" + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + workplace2: + auth: + key-management: eap + method: peap + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + ca-certificate: /etc/ssl/work2-cacrt.pem + workplacehashed: + auth: + key-management: eap + method: ttls + anonymous-identity: "@internal.example.com" + identity: "joe@internal.example.com" + password: hash:9db1636cedc5948537e7bee0cc1e9590 + customernet: + auth: + key-management: eap + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + opennet: + auth: + key-management: none + peer2peer: + mode: adhoc + auth: {} + dhcp4: yes + ''') + + self.assert_networkd({'wl0.network': ND_WIFI_DHCP4 % 'wl0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:wl0,''') + self.assert_nm_udev(None) + + # generates wpa config and enables wpasupplicant unit + with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: + new_config = f.read() + self.assertIn('ctrl_interface=/run/wpa_supplicant', new_config) + self.assertIn(''' +network={ + ssid="peer2peer" + mode=1 + key_mgmt=NONE +} +''', new_config) + self.assertIn(''' +network={ + ssid="Luke's Home" + key_mgmt=WPA-PSK + psk="4lsos3kr1t" +} +''', new_config) + self.assertIn(''' +network={ + ssid="BobsHome" + key_mgmt=WPA-PSK + psk=e03ce667c87bc81ca968d9120ca37f89eb09aec3c55b80386e5d772efd6b926e +} +''', new_config) + self.assertIn(''' +network={ + ssid="BillsHome" + key_mgmt=WPA-PSK + psk=db3b0acf5653aeaddd5fe034fb9f07175b2864f847b005aaa2f09182d9411b04 +} +''', new_config) + self.assertIn(''' +network={ + ssid="workplace2" + key_mgmt=WPA-EAP + eap=PEAP + identity="joe@internal.example.com" + password="v3ryS3kr1t" + ca_cert="/etc/ssl/work2-cacrt.pem" +} +''', new_config) + self.assertIn(''' +network={ + ssid="workplace" + key_mgmt=WPA-EAP + eap=TTLS + identity="joe@internal.example.com" + anonymous_identity="@internal.example.com" + password="v3ryS3kr1t" +} +''', new_config) + self.assertIn(''' +network={ + ssid="workplacehashed" + key_mgmt=WPA-EAP + eap=TTLS + identity="joe@internal.example.com" + anonymous_identity="@internal.example.com" + password=hash:9db1636cedc5948537e7bee0cc1e9590 +} +''', new_config) + self.assertIn(''' +network={ + ssid="customernet" + key_mgmt=WPA-EAP + eap=TLS + identity="cert-joe@cust.example.com" + anonymous_identity="@cust.example.com" + ca_cert="/etc/ssl/cust-cacrt.pem" + client_cert="/etc/ssl/cust-crt.pem" + private_key="/etc/ssl/cust-key.pem" + private_key_passwd="d3cryptPr1v4t3K3y" +} +''', new_config) + self.assertIn(''' +network={ + ssid="opennet" + key_mgmt=NONE +} +''', new_config) + self.assertIn(''' +network={ + ssid="Joe's Home" + key_mgmt=WPA-PSK + psk="s0s3kr1t" +} +''', new_config) + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + + def test_auth_wired(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + auth: + key-management: 802.1x + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + phase2-auth: MSCHAPV2 + dhcp4: yes + ''') + + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + self.assert_nm_udev(None) + + # generates wpa config and enables wpasupplicant unit + with open(os.path.join(self.workdir.name, 'run/netplan/wpa-eth0.conf')) as f: + self.assertEqual(f.read(), '''ctrl_interface=/run/wpa_supplicant + +network={ + key_mgmt=IEEE8021X + eap=TLS + identity="cert-joe@cust.example.com" + anonymous_identity="@cust.example.com" + ca_cert="/etc/ssl/cust-cacrt.pem" + client_cert="/etc/ssl/cust-crt.pem" + private_key="/etc/ssl/cust-key.pem" + private_key_passwd="d3cryptPr1v4t3K3y" + phase2="auth=MSCHAPV2" +} +''') + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-eth0.service'))) + + +class TestNetworkManager(TestBase): + + def test_auth_wifi_detailed(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + "Joe's Home": + password: "s0s3kr1t" + "Luke's Home": + auth: + key-management: psk + password: "4lsos3kr1t" + workplace: + auth: + key-management: eap + method: ttls + anonymous-identity: "@internal.example.com" + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + workplace2: + auth: + key-management: eap + method: peap + identity: "joe@internal.example.com" + password: "v3ryS3kr1t" + ca-certificate: /etc/ssl/work2-cacrt.pem + workplacehashed: + auth: + key-management: eap + method: ttls + anonymous-identity: "@internal.example.com" + identity: "joe@internal.example.com" + password: hash:9db1636cedc5948537e7bee0cc1e9590 + customernet: + auth: + key-management: 802.1x + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + phase2-auth: MSCHAPV2 + opennet: + auth: + key-management: none + peer2peer: + mode: adhoc + auth: {} + dhcp4: yes + ''') + + self.assert_networkd({}) + self.assert_nm({'wl0-Joe%27s%20Home': '''[connection] +id=netplan-wl0-Joe's Home +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=Joe's Home +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-psk +psk=s0s3kr1t +''', + 'wl0-Luke%27s%20Home': '''[connection] +id=netplan-wl0-Luke's Home +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=Luke's Home +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-psk +psk=4lsos3kr1t +''', + 'wl0-workplace': '''[connection] +id=netplan-wl0-workplace +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=workplace +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-eap + +[802-1x] +eap=ttls +identity=joe@internal.example.com +anonymous-identity=@internal.example.com +password=v3ryS3kr1t +''', + 'wl0-workplace2': '''[connection] +id=netplan-wl0-workplace2 +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=workplace2 +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-eap + +[802-1x] +eap=peap +identity=joe@internal.example.com +password=v3ryS3kr1t +ca-cert=/etc/ssl/work2-cacrt.pem +''', + 'wl0-workplacehashed': '''[connection] +id=netplan-wl0-workplacehashed +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=workplacehashed +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-eap + +[802-1x] +eap=ttls +identity=joe@internal.example.com +anonymous-identity=@internal.example.com +password=hash:9db1636cedc5948537e7bee0cc1e9590 +''', + 'wl0-customernet': '''[connection] +id=netplan-wl0-customernet +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=customernet +mode=infrastructure + +[wifi-security] +key-mgmt=ieee8021x + +[802-1x] +eap=tls +identity=cert-joe@cust.example.com +anonymous-identity=@cust.example.com +ca-cert=/etc/ssl/cust-cacrt.pem +client-cert=/etc/ssl/cust-crt.pem +private-key=/etc/ssl/cust-key.pem +private-key-password=d3cryptPr1v4t3K3y +phase2-auth=MSCHAPV2 +''', + 'wl0-opennet': '''[connection] +id=netplan-wl0-opennet +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=opennet +mode=infrastructure +''', + 'wl0-peer2peer': '''[connection] +id=netplan-wl0-peer2peer +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=peer2peer +mode=adhoc +'''}) + self.assert_nm_udev(None) + + def test_auth_wired(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + auth: + key-management: 802.1x + method: tls + anonymous-identity: "@cust.example.com" + identity: "cert-joe@cust.example.com" + ca-certificate: /etc/ssl/cust-cacrt.pem + client-certificate: /etc/ssl/cust-crt.pem + client-key: /etc/ssl/cust-key.pem + client-key-password: "d3cryptPr1v4t3K3y" + dhcp4: yes + ''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[802-1x] +eap=tls +identity=cert-joe@cust.example.com +anonymous-identity=@cust.example.com +ca-cert=/etc/ssl/cust-cacrt.pem +client-cert=/etc/ssl/cust-crt.pem +private-key=/etc/ssl/cust-key.pem +private-key-password=d3cryptPr1v4t3K3y +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + +class TestConfigErrors(TestBase): + + def test_auth_invalid_key_mgmt(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + auth: + key-management: bogus''', expect_fail=True) + self.assertIn("unknown key management type 'bogus'", err) + + def test_auth_invalid_eap_method(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + auth: + method: bogus''', expect_fail=True) + self.assertIn("unknown EAP method 'bogus'", err) + + def test_auth_networkd_wifi_psk_too_big(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "LoremipsumdolorsitametconsecteturadipiscingelitCrastemporvelitnunc" + dhcp4: yes''', expect_fail=True) + self.assertIn("ASCII passphrase must be between 8 and 63 characters (inclusive)", err) + + def test_auth_networkd_wifi_psk_too_small(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "p4ss" + dhcp4: yes''', expect_fail=True) + self.assertIn("ASCII passphrase must be between 8 and 63 characters (inclusive)", err) + + def test_auth_networkd_wifi_psk_64_non_hexdigit(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "LoremipsumdolorsitametconsecteturadipiscingelitCrastemporvelitnu" + dhcp4: yes''', expect_fail=True) + self.assertIn("PSK length of 64 is only supported for hex-digit representation", err) diff --git a/tests/generator/test_bonds.py b/tests/generator/test_bonds.py new file mode 100644 index 0000000..fea475e --- /dev/null +++ b/tests/generator/test_bonds.py @@ -0,0 +1,812 @@ +# +# Tests for bond devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import TestBase + + +class TestNetworkd(TestBase): + + def test_bond_dhcp6_no_accept_ra(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: no + accept-ra: no + bonds: + bond0: + interfaces: [engreen] + dhcp6: true + accept-ra: yes''') + self.assert_networkd({'bond0.network': '''[Match] +Name=bond0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6AcceptRA=yes +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'bond0.netdev': '''[NetDev] +Name=bond0 +Kind=bond +''', + 'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=no +IPv6AcceptRA=no +Bond=bond0 +'''}) + + def test_bond_empty(self): + self.generate('''network: + version: 2 + bonds: + bn0: + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:bn0,''') + self.assert_nm_udev(None) + + def test_bond_components(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_empty_parameters(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: {} + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_with_parameters(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: + mode: 802.3ad + lacp-rate: 10 + mii-monitor-interval: 10 + min-links: 10 + up-delay: 20 + down-delay: 30 + all-slaves-active: true + transmit-hash-policy: none + ad-select: none + arp-interval: 15 + arp-validate: all + arp-all-targets: all + fail-over-mac-policy: none + gratuitious-arp: 10 + packets-per-slave: 10 + primary-reselect-policy: none + resend-igmp: 10 + learn-packet-interval: 10 + arp-ip-targets: + - 10.10.10.10 + - 20.20.20.20 + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' + '[Bond]\n' + 'Mode=802.3ad\n' + 'LACPTransmitRate=10\n' + 'MIIMonitorSec=10ms\n' + 'MinLinks=10\n' + 'TransmitHashPolicy=none\n' + 'AdSelect=none\n' + 'AllSlavesActive=1\n' + 'ARPIntervalSec=15ms\n' + 'ARPIPTargets=10.10.10.10 20.20.20.20\n' + 'ARPValidate=all\n' + 'ARPAllTargets=all\n' + 'UpDelaySec=20ms\n' + 'DownDelaySec=30ms\n' + 'FailOverMACPolicy=none\n' + 'GratuitousARP=10\n' + 'PacketsPerSlave=10\n' + 'PrimaryReselectPolicy=none\n' + 'ResendIGMP=10\n' + 'LearnPacketIntervalSec=10\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_with_parameters_all_suffix(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: + mode: 802.3ad + mii-monitor-interval: 10ms + up-delay: 20ms + down-delay: 30s + arp-interval: 15m + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' + '[Bond]\n' + 'Mode=802.3ad\n' + 'MIIMonitorSec=10ms\n' + 'ARPIntervalSec=15m\n' + 'UpDelaySec=20ms\n' + 'DownDelaySec=30s\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_primary_slave(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: + mode: active-backup + primary: eno1 + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' + '[Bond]\n' + 'Mode=active-backup\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\nPrimarySlave=true\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + def test_bond_primary_slave_duplicate(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + eno1: {} + enp65s0: {} + dummy2: {} + bonds: + bond0: + interfaces: [eno1, enp65s0] + parameters: + primary: enp65s0 + mode: balance-tlb + vlans: + vbr-v10: + id: 10 + link: vbr + bridges: + vbr: + interfaces: [dummy2]''', expect_fail=False) + + self.assert_networkd({'eno1.network': '[Match]\nName=eno1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'enp65s0.network': '''[Match] +Name=enp65s0 + +[Network] +LinkLocalAddressing=no +Bond=bond0 +PrimarySlave=true +''', + 'dummy2.network': '[Match]\nName=dummy2\n\n[Network]\nLinkLocalAddressing=no\nBridge=vbr\n', + 'bond0.network': '[Match]\nName=bond0\n\n' + '[Network]\nLinkLocalAddressing=ipv6\nConfigureWithoutCarrier=yes\n', + 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n\n[Bond]\nMode=balance-tlb\n', + 'vbr-v10.network': '[Match]\nName=vbr-v10\n\n' + '[Network]\nLinkLocalAddressing=ipv6\nConfigureWithoutCarrier=yes\n', + 'vbr-v10.netdev': '[NetDev]\nName=vbr-v10\nKind=vlan\n\n[VLAN]\nId=10\n', + 'vbr.network': '[Match]\nName=vbr\n\n' + '[Network]\nLinkLocalAddressing=ipv6\nConfigureWithoutCarrier=yes\nVLAN=vbr-v10\n', + 'vbr.netdev': '[NetDev]\nName=vbr\nKind=bridge\n'}) + + def test_bond_with_gratuitous_spelling(self): + """Validate that the correct spelling of gratuitous also works""" + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bonds: + bn0: + parameters: + mode: active-backup + gratuitous-arp: 10 + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'bn0.netdev': '[NetDev]\nName=bn0\nKind=bond\n\n' + '[Bond]\n' + 'Mode=active-backup\n' + 'GratuitousARP=10\n', + 'bn0.network': '''[Match] +Name=bn0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBond=bn0\n'}) + + +class TestNetworkManager(TestBase): + + def test_bond_empty(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bonds: + bn0: + dhcp4: true''') + + self.assert_nm({'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + + def test_bond_components(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bonds: + bn0: + interfaces: [eno1, switchport] + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bond_empty_params(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bonds: + bn0: + interfaces: [eno1, switchport] + parameters: {} + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bond_with_params(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bonds: + bn0: + interfaces: [eno1, switchport] + parameters: + mode: 802.3ad + lacp-rate: 10 + mii-monitor-interval: 10 + min-links: 10 + up-delay: 10 + down-delay: 10 + all-slaves-active: true + transmit-hash-policy: none + ad-select: none + arp-interval: 10 + arp-validate: all + arp-all-targets: all + arp-ip-targets: + - 10.10.10.10 + - 20.20.20.20 + fail-over-mac-policy: none + gratuitious-arp: 10 + packets-per-slave: 10 + primary-reselect-policy: none + resend-igmp: 10 + learn-packet-interval: 10 + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[bond] +mode=802.3ad +lacp_rate=10 +miimon=10 +min_links=10 +xmit_hash_policy=none +ad_select=none +all_slaves_active=1 +arp_interval=10 +arp_ip_target=10.10.10.10,20.20.20.20 +arp_validate=all +arp_all_targets=all +updelay=10 +downdelay=10 +fail_over_mac=none +num_grat_arp=10 +num_unsol_na=10 +packets_per_slave=10 +primary_reselect=none +resend_igmp=10 +lp_interval=10 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bond_primary_slave(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bonds: + bn0: + interfaces: [eno1, switchport] + parameters: + mode: active-backup + primary: eno1 + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bond +master=bn0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bn0': '''[connection] +id=netplan-bn0 +type=bond +interface-name=bn0 + +[bond] +mode=active-backup +primary=eno1 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + +class TestConfigErrors(TestBase): + + def test_bond_invalid_mode(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bonds: + bond0: + interfaces: [eno1] + parameters: + mode: lacp + arp-ip-targets: + - 2001:dead:beef::1 + dhcp4: true''', expect_fail=True) + self.assertIn("unknown bond mode 'lacp'", err) + + def test_bond_invalid_arp_target(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bonds: + bond0: + interfaces: [eno1] + parameters: + arp-ip-targets: + - 2001:dead:beef::1 + dhcp4: true''', expect_fail=True) + + def test_bond_invalid_primary_slave(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bonds: + bond0: + interfaces: [eno1] + parameters: + primary: wigglewiggle + dhcp4: true''', expect_fail=True) + + def test_bond_duplicate_primary_slave(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + eno2: + match: + name: eth1 + bonds: + bond0: + interfaces: [eno1, eno2] + parameters: + primary: eno1 + primary: eno2 + dhcp4: true''', expect_fail=True) + + def test_bond_multiple_assignments(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bonds: + bond0: + interfaces: [eno1] + bond1: + interfaces: [eno1]''', expect_fail=True) + self.assertIn("bond1: interface 'eno1' is already assigned to bond bond0", err) + + def test_bond_bridge_cross_assignments1(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bonds: + bond0: + interfaces: [eno1] + bridges: + br1: + interfaces: [eno1]''', expect_fail=True) + self.assertIn("br1: interface 'eno1' is already assigned to bond bond0", err) + + def test_bond_bridge_cross_assignments2(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bridges: + br0: + interfaces: [eno1] + bonds: + bond1: + interfaces: [eno1]''', expect_fail=True) + self.assertIn("bond1: interface 'eno1' is already assigned to bridge br0", err) + + def test_bond_bridge_nested_assignments(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + bonds: + bond0: + interfaces: [eno1] + bridges: + br1: + interfaces: [bond0]''') diff --git a/tests/generator/test_bridges.py b/tests/generator/test_bridges.py new file mode 100644 index 0000000..7898cbe --- /dev/null +++ b/tests/generator/test_bridges.py @@ -0,0 +1,733 @@ +# +# Tests for bridge devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import unittest + +from .base import TestBase + + +class TestNetworkd(TestBase): + + def test_bridge_set_mac(self): + self.generate('''network: + version: 2 + bridges: + br0: + macaddress: 00:01:02:03:04:05 + dhcp4: true''') + + self.assert_networkd({'br0.network': '''[Match] +Name=br0 + +[Link] +MACAddress=00:01:02:03:04:05 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'br0.netdev': '[NetDev]\nName=br0\nMACAddress=00:01:02:03:04:05\nKind=bridge\n'}) + + def test_bridge_dhcp6_no_accept_ra(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: no + dhcp6: no + accept-ra: no + bridges: + br0: + interfaces: [engreen] + dhcp6: true + accept-ra: no''') + self.assert_networkd({'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6AcceptRA=no +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'br0.netdev': '''[NetDev] +Name=br0 +Kind=bridge +''', + 'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=no +IPv6AcceptRA=no +Bridge=br0 +'''}) + + def test_bridge_empty(self): + self.generate('''network: + version: 2 + bridges: + br0: + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:br0,''') + self.assert_nm_udev(None) + + def test_bridge_type_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + renderer: networkd + br0: + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:br0,''') + self.assert_nm_udev(None) + + def test_bridge_def_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + renderer: NetworkManager + br0: + renderer: networkd + addresses: [1.2.3.4/12] + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +Address=1.2.3.4/12 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:br0,''') + self.assert_nm_udev(None) + + def test_bridge_forward_declaration(self): + self.generate('''network: + version: 2 + bridges: + br0: + interfaces: [eno1, switchports] + dhcp4: true + ethernets: + eno1: {} + switchports: + match: + driver: yayroute +''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) + + @unittest.skipIf("CODECOV_TOKEN" in os.environ, "Skip on codecov.io: GLib changed hashtable sorting") + def test_eth_bridge_nm_blacklist(self): # pragma: nocover + self.generate('''network: + renderer: networkd + ethernets: + eth42: + dhcp4: yes + ethbr: + match: {name: eth43} + bridges: + mybr: + interfaces: [ethbr] + dhcp4: yes''') + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth42,interface-name:eth43,interface-name:mybr,''') + + def test_bridge_components(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bridges: + br0: + interfaces: [eno1, switchports] + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) + + def test_bridge_params(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute + bridges: + br0: + interfaces: [eno1, switchports] + parameters: + ageing-time: 50 + forward-delay: 12 + hello-time: 6 + max-age: 24 + priority: 1000 + stp: true + path-cost: + eno1: 70 + port-priority: + eno1: 14 + dhcp4: true''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' + '[Bridge]\nAgeingTimeSec=50\n' + 'Priority=1000\n' + 'ForwardDelaySec=12\n' + 'HelloTimeSec=6\n' + 'MaxAgeSec=24\n' + 'STP=true\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' + '[Bridge]\nCost=70\nPriority=14\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) + + +class TestNetworkManager(TestBase): + + def test_bridge_empty(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + br0: + dhcp4: true''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_type_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + bridges: + renderer: NetworkManager + br0: + dhcp4: true''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_set_mac(self): + self.generate('''network: + version: 2 + bridges: + renderer: NetworkManager + br0: + macaddress: 00:01:02:03:04:05 + dhcp4: true''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ethernet] +cloned-mac-address=00:01:02:03:04:05 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + + def test_bridge_def_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + bridges: + renderer: networkd + br0: + renderer: NetworkManager + addresses: [1.2.3.4/12] + dhcp4: true''') + + self.assert_nm({'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto +address1=1.2.3.4/12 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_forward_declaration(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + bridges: + br0: + interfaces: [eno1, switchport] + dhcp4: true + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 +''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_components(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bridges: + br0: + interfaces: [eno1, switchport] + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_bridge_params(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eno1: {} + switchport: + match: + name: enp2s1 + bridges: + br0: + interfaces: [eno1, switchport] + parameters: + ageing-time: 50 + priority: 1000 + forward-delay: 12 + hello-time: 6 + max-age: 24 + path-cost: + eno1: 70 + port-priority: + eno1: 61 + stp: true + dhcp4: true''') + + self.assert_nm({'eno1': '''[connection] +id=netplan-eno1 +type=ethernet +interface-name=eno1 +slave-type=bridge +master=br0 + +[bridge-port] +path-cost=70 +priority=61 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'switchport': '''[connection] +id=netplan-switchport +type=ethernet +interface-name=enp2s1 +slave-type=bridge +master=br0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'br0': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[bridge] +ageing-time=50 +priority=1000 +forward-delay=12 +hello-time=6 +max-age=24 +stp=true + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + +class TestNetplanYAMLv2(TestBase): + '''No asserts are needed. + + The generate() method implicitly checks the (re-)generated YAML. + ''' + + def test_bridge_stp(self): + self.generate('''network: + version: 2 + bridges: + br0: + parameters: + stp: no + dhcp4: true''') + + +class TestConfigErrors(TestBase): + + def test_bridge_unknown_iface(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + interfaces: ['foo']''', expect_fail=True) + self.assertIn("br0: interface 'foo' is not defined", err) + + def test_bridge_multiple_assignments(self): + err = self.generate('''network: + version: 2 + ethernets: + eno1: {} + bridges: + br0: + interfaces: [eno1] + br1: + interfaces: [eno1]''', expect_fail=True) + self.assertIn("br1: interface 'eno1' is already assigned to bridge br0", err) + + def test_bridge_invalid_dev_for_path_cost(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + path-cost: + eth0: 50 + dhcp4: true''', expect_fail=True) + + def test_bridge_path_cost_already_defined(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + path-cost: + eno1: 50 + eno1: 40 + dhcp4: true''', expect_fail=True) + + def test_bridge_invalid_path_cost(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + path-cost: + eno1: aa + dhcp4: true''', expect_fail=True) + + def test_bridge_invalid_dev_for_port_prio(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + port-priority: + eth0: 50 + dhcp4: true''', expect_fail=True) + + def test_bridge_port_prio_already_defined(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + port-priority: + eno1: 50 + eno1: 40 + dhcp4: true''', expect_fail=True) + + def test_bridge_invalid_port_prio(self): + self.generate('''network: + version: 2 + ethernets: + eno1: + match: + name: eth0 + bridges: + br0: + interfaces: [eno1] + parameters: + port-priority: + eno1: 257 + dhcp4: true''', expect_fail=True) diff --git a/tests/generator/test_common.py b/tests/generator/test_common.py new file mode 100644 index 0000000..7bdb4b4 --- /dev/null +++ b/tests/generator/test_common.py @@ -0,0 +1,1690 @@ +# +# Common tests for netplan generator +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import textwrap + +from .base import TestBase, ND_DHCP4, ND_DHCP6, ND_DHCPYES, ND_EMPTY + + +class TestNetworkd(TestBase): + '''networkd output''' + + def test_optional(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + optional: true''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Link] +RequiredForOnline=no + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_networkd_udev(None) + + def config_with_optional_addresses(self, eth_name, optional_addresses): + return '''network: + version: 2 + ethernets: + {}: + dhcp6: true + optional-addresses: {}'''.format(eth_name, optional_addresses) + + def test_optional_addresses(self): + eth_name = self.eth_name() + self.generate(self.config_with_optional_addresses(eth_name, '["dhcp4"]')) + self.assertEqual(self.get_optional_addresses(eth_name), set(["dhcp4"])) + + def test_optional_addresses_multiple(self): + eth_name = self.eth_name() + self.generate(self.config_with_optional_addresses(eth_name, '[dhcp4, ipv4-ll, ipv6-ra, dhcp6, dhcp4, static]')) + self.assertEqual( + self.get_optional_addresses(eth_name), + set(["ipv4-ll", "ipv6-ra", "dhcp4", "dhcp6", "static"])) + + def test_optional_addresses_invalid(self): + eth_name = self.eth_name() + err = self.generate(self.config_with_optional_addresses(eth_name, '["invalid"]'), expect_fail=True) + self.assertIn('invalid value for optional-addresses', err) + + def test_activation_mode_off(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + activation-mode: off''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Link] +ActivationPolicy=always-down +RequiredForOnline=no + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_networkd_udev(None) + + def test_activation_mode_manual(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + activation-mode: manual''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Link] +ActivationPolicy=manual +RequiredForOnline=no + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_networkd_udev(None) + + def test_mtu_all(self): + self.generate(textwrap.dedent(""" + network: + version: 2 + ethernets: + eth1: + mtu: 9000 + dhcp4: n + ipv6-mtu: 2000 + bonds: + bond0: + interfaces: + - eth1 + mtu: 9000 + vlans: + bond0.108: + link: bond0 + id: 108""")) + self.assert_networkd({ + 'bond0.108.netdev': '[NetDev]\nName=bond0.108\nKind=vlan\n\n[VLAN]\nId=108\n', + 'bond0.108.network': '''[Match] +Name=bond0.108 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +''', + 'bond0.netdev': '[NetDev]\nName=bond0\nMTUBytes=9000\nKind=bond\n', + 'bond0.network': '''[Match] +Name=bond0 + +[Link] +MTUBytes=9000 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +VLAN=bond0.108 +''', + 'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=9000\n', + 'eth1.network': '''[Match] +Name=eth1 + +[Link] +MTUBytes=9000 + +[Network] +LinkLocalAddressing=no +IPv6MTUBytes=2000 +Bond=bond0 +''' + }) + self.assert_networkd_udev(None) + + def test_eth_global_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + eth0: + dhcp4: true''') + + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + self.assert_nm_udev(None) + # should not allow NM to manage everything + self.assertFalse(os.path.exists(self.nm_enable_all_conf)) + + def test_eth_type_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + renderer: networkd + eth0: + dhcp4: true''') + + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + # should allow NM to manage everything else + self.assertTrue(os.path.exists(self.nm_enable_all_conf)) + self.assert_nm_udev(None) + + def test_eth_def_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + renderer: NetworkManager + eth0: + renderer: networkd + dhcp4: true''') + + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + self.assert_nm_udev(None) + + def test_eth_dhcp6(self): + self.generate('''network: + version: 2 + ethernets: + eth0: {dhcp6: true}''') + self.assert_networkd({'eth0.network': ND_DHCP6 % 'eth0'}) + + def test_eth_dhcp6_no_accept_ra(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + accept-ra: n''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6AcceptRA=no + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_dhcp6_accept_ra(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + accept-ra: yes''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6AcceptRA=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_dhcp6_accept_ra_unset(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_dhcp4_and_6(self): + self.generate('''network: + version: 2 + ethernets: + eth0: {dhcp4: true, dhcp6: true}''') + self.assert_networkd({'eth0.network': ND_DHCPYES % 'eth0'}) + + def test_eth_manual_addresses(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +Address=2001:FFfe::1/64 +'''}) + + def test_eth_manual_addresses_dhcp(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +Address=2001:FFfe::1/64 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_address_option_lifetime_zero(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.2/24: + lifetime: 0 + - 2001:FFfe::1/64''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=2001:FFfe::1/64 + +[Address] +Address=192.168.14.2/24 +PreferredLifetime=0 +'''}) + + def test_eth_address_option_lifetime_forever(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.2/24: + lifetime: forever + - 2001:FFfe::1/64''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=2001:FFfe::1/64 + +[Address] +Address=192.168.14.2/24 +PreferredLifetime=forever +'''}) + + def test_eth_address_option_label(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.2/24: + label: test-label + - 2001:FFfe::1/64''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=2001:FFfe::1/64 + +[Address] +Address=192.168.14.2/24 +Label=test-label +'''}) + + def test_eth_address_option_multi_pass(self): + self.generate('''network: + version: 2 + bridges: + br0: + interfaces: [engreen] + ethernets: + engreen: + addresses: + - 192.168.14.2/24: + label: test-label + - 2001:FFfe::1/64: + label: ip6''') + + self.assert_networkd({ + 'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=no +Bridge=br0 + +[Address] +Address=192.168.14.2/24 +Label=test-label + +[Address] +Address=2001:FFfe::1/64 +Label=ip6 +''', + 'br0.network': ND_EMPTY % ('br0', 'ipv6'), + 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n'}) + + def test_bond_arp_ip_targets_multi_pass(self): + self.generate('''network: + bonds: + bond0: + interfaces: + - eno1 + parameters: + arp-ip-targets: + - 10.10.10.10 + - 20.20.20.20 + ethernets: + eno1: {} + version: 2''') + self.assert_networkd({'bond0.netdev': '''[NetDev] +Name=bond0 +Kind=bond + +[Bond] +ARPIPTargets=10.10.10.10 20.20.20.20 +''', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +''', + 'eno1.network': '''[Match] +Name=eno1 + +[Network] +LinkLocalAddressing=no +Bond=bond0 +'''}) + + def test_dhcp_critical_true(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + critical: yes +''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 + +[DHCP] +CriticalConnection=true +'''}) + + def test_dhcp_identifier_mac(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp-identifier: mac +''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +ClientIdentifier=mac +RouteMetric=100 +UseMTU=true +'''}) + + def test_dhcp_identifier_duid(self): + # This option should be silently ignored, since it's the default + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp-identifier: duid +''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_eth_ipv6_privacy(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp6: true + ipv6-privacy: true''') + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6PrivacyExtensions=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_gateway4(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + gateway4: 192.168.14.1''') + self.assertIn("`gateway4` has been deprecated, use default routes instead.", err) + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +Gateway=192.168.14.1 +'''}) + + def test_gateway6(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["2001:FFfe::1/64"] + gateway6: 2001:FFfe::2''') + self.assertIn("`gateway6` has been deprecated, use default routes instead.", err) + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=2001:FFfe::1/64 +Gateway=2001:FFfe::2 +'''}) + + def test_gateway_full(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] + gateway4: 192.168.14.1 + gateway6: "2001:FFfe::2"''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +Address=2001:FFfe::1/64 +Gateway=192.168.14.1 +Gateway=2001:FFfe::2 +'''}) + + def test_gateways_multi_pass(self): + self.generate('''network: + version: 2 + bridges: + br0: + interfaces: [engreen] + ethernets: + engreen: + addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] + gateway4: 192.168.14.1 + gateway6: "2001:FFfe::2"''') + + self.assert_networkd({ + 'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=no +Address=192.168.14.2/24 +Address=2001:FFfe::1/64 +Gateway=192.168.14.1 +Gateway=2001:FFfe::2 +Bridge=br0 +''', + 'br0.network': ND_EMPTY % ('br0', 'ipv6'), + 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n'}) + + def test_nameserver(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + nameservers: + addresses: [1.2.3.4, "1234::FFFF"] + enblue: + addresses: ["192.168.1.3/24"] + nameservers: + search: [lab, kitchen] + addresses: [8.8.8.8]''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 +DNS=1.2.3.4 +DNS=1234::FFFF +''', + 'enblue.network': '''[Match] +Name=enblue + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.1.3/24 +DNS=8.8.8.8 +Domains=lab kitchen +'''}) + + def test_link_local_all(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ ipv4, ipv6 ] + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_link_local_ipv4(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ ipv4 ] + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=ipv4 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_link_local_ipv6(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ ipv6 ] + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_link_local_disabled(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ ] + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=no + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_ip6_addr_gen_mode(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + enblue: + dhcp6: yes + ipv6-address-generation: eui64''') + self.assert_networkd({'enblue.network': '''[Match]\nName=enblue\n +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + def test_ip6_addr_gen_token(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + engreen: + dhcp6: yes + ipv6-address-token: ::2 + enblue: + dhcp6: yes + ipv6-address-token: "::2"''') + self.assert_networkd({'engreen.network': '''[Match]\nName=engreen\n +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6Token=static:::2 + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'enblue.network': '''[Match]\nName=enblue\n +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 +IPv6Token=static:::2 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + +class TestNetworkManager(TestBase): + + def test_mtu_all(self): + self.generate(textwrap.dedent(""" + network: + version: 2 + renderer: NetworkManager + ethernets: + eth1: + mtu: 1280 + dhcp4: n + bonds: + bond0: + interfaces: + - eth1 + mtu: 9000 + vlans: + bond0.108: + link: bond0 + id: 108""")) + self.assert_nm({ + 'bond0.108': '''[connection] +id=netplan-bond0.108 +type=vlan +interface-name=bond0.108 + +[vlan] +id=108 +parent=bond0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'bond0': '''[connection] +id=netplan-bond0 +type=bond +interface-name=bond0 + +[ethernet] +mtu=9000 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'eth1': '''[connection] +id=netplan-eth1 +type=ethernet +interface-name=eth1 +slave-type=bond +master=bond0 + +[ethernet] +wake-on-lan=0 +mtu=1280 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + }) + + def test_activation_mode_off(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + activation-mode: off''', expect_fail=True) + + def test_activation_mode_manual(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + activation-mode: manual''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +autoconnect=false +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_ipv6_mtu(self): + self.generate(textwrap.dedent(""" + network: + version: 2 + renderer: NetworkManager + ethernets: + eth1: + mtu: 9000 + ipv6-mtu: 2000"""), expect_fail=True) + + def test_eth_global_renderer(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + dhcp4: true''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_type_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + renderer: NetworkManager + eth0: + dhcp4: true''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_def_renderer(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + renderer: networkd + eth0: + renderer: NetworkManager''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_global_renderer_only(self): + self.generate(None, confs={'01-default-nm.yaml': 'network: {version: 2, renderer: NetworkManager}'}) + # should allow NM to manage everything else + self.assertTrue(os.path.exists(self.nm_enable_all_conf)) + # but not configure anything else + self.assert_nm(None, None) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_dhcp6(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: {dhcp6: true}''') + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=auto +'''}) + + def test_eth_dhcp4_and_6(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: {dhcp4: true, dhcp6: true}''') + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=auto +'''}) + + def test_ip6_addr_gen_mode(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp6: yes + ipv6-address-generation: stable-privacy + enblue: + dhcp6: yes + ipv6-address-generation: eui64''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=auto +addr-gen-mode=1 +''', + 'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=auto +addr-gen-mode=0 +'''}) + + def test_ip6_addr_gen_token(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp6: yes + ipv6-address-token: ::2 + enblue: + dhcp6: yes + ipv6-address-token: "::2"''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=auto +addr-gen-mode=0 +token=::2 +''', + 'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=auto +addr-gen-mode=0 +token=::2 +'''}) + + def test_eth_manual_addresses(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: + - 192.168.14.2/24 + - 172.16.0.4/16 + - 2001:FFfe::1/64''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +address2=172.16.0.4/16 + +[ipv6] +method=manual +address1=2001:FFfe::1/64 +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_manual_addresses_dhcp(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: yes + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto +address1=192.168.14.2/24 + +[ipv6] +method=manual +address1=2001:FFfe::1/64 +'''}) + + def test_eth_ipv6_privacy(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: {dhcp6: true, ipv6-privacy: true}''') + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=auto +ip6-privacy=2 +'''}) + + def test_gateway(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24", "2001:FFfe::1/64"] + gateway4: 192.168.14.1 + gateway6: 2001:FFfe::2''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +gateway=192.168.14.1 + +[ipv6] +method=manual +address1=2001:FFfe::1/64 +gateway=2001:FFfe::2 +'''}) + + def test_nameserver(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + nameservers: + addresses: [1.2.3.4, 2.3.4.5, "1234::FFFF"] + search: [lab, kitchen] + enblue: + addresses: ["192.168.1.3/24"] + nameservers: + addresses: [8.8.8.8]''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +dns=1.2.3.4;2.3.4.5; +dns-search=lab;kitchen; + +[ipv6] +method=manual +dns=1234::FFFF; +dns-search=lab;kitchen; +''', + 'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.1.3/24 +dns=8.8.8.8; + +[ipv6] +method=ignore +'''}) + + +class TestForwardDeclaration(TestBase): + + def test_fwdecl_bridge_on_bond(self): + self.generate('''network: + version: 2 + bridges: + br0: + interfaces: ['bond0'] + dhcp4: true + bonds: + bond0: + interfaces: ['eth0', 'eth1'] + ethernets: + eth0: + match: + macaddress: 00:01:02:03:04:05 + set-name: eth0 + eth1: + match: + macaddress: 02:01:02:03:04:05 + set-name: eth1 +''') + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bridge=br0 +''', + 'eth0.link': '''[Match] +MACAddress=00:01:02:03:04:05 +Type=!vlan bond bridge + +[Link] +Name=eth0 +WakeOnLan=off +''', + 'eth0.network': '''[Match] +MACAddress=00:01:02:03:04:05 +Name=eth0 +Type=!vlan bond bridge + +[Network] +LinkLocalAddressing=no +Bond=bond0 +''', + 'eth1.link': '''[Match] +MACAddress=02:01:02:03:04:05 +Type=!vlan bond bridge + +[Link] +Name=eth1 +WakeOnLan=off +''', + 'eth1.network': '''[Match] +MACAddress=02:01:02:03:04:05 +Name=eth1 +Type=!vlan bond bridge + +[Network] +LinkLocalAddressing=no +Bond=bond0 +'''}) + + def test_fwdecl_feature_blend(self): + self.generate('''network: + version: 2 + vlans: + vlan1: + link: 'br0' + id: 1 + dhcp4: true + bridges: + br0: + interfaces: ['bond0', 'eth2'] + parameters: + path-cost: + eth2: 1000 + bond0: 8888 + bonds: + bond0: + interfaces: ['eth0', 'br1'] + ethernets: + eth0: + match: + macaddress: 00:01:02:03:04:05 + set-name: eth0 + bridges: + br1: + interfaces: ['eth1'] + ethernets: + eth1: + match: + macaddress: 02:01:02:03:04:05 + set-name: eth1 + eth2: + match: + name: eth2 +''', skip_generated_yaml_validation=True) + # XXX: We need to skeip the generated YAML validation, as the pyYAML + # parser overrides the duplicate "ethernets"/"bridges" keys, while + # the netplan C YAML parser merges them into the netdef + + self.assert_networkd({'vlan1.netdev': '[NetDev]\nName=vlan1\nKind=vlan\n\n' + '[VLAN]\nId=1\n', + 'vlan1.network': '''[Match] +Name=vlan1 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n\n' + '[Bridge]\nSTP=true\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +VLAN=vlan1 +''', + 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bridge=br0 + +[Bridge] +Cost=8888 +''', + 'eth2.network': '[Match]\nName=eth2\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n\n' + '[Bridge]\nCost=1000\n', + 'br1.netdev': '[NetDev]\nName=br1\nKind=bridge\n', + 'br1.network': '''[Match] +Name=br1 + +[Network] +LinkLocalAddressing=no +ConfigureWithoutCarrier=yes +Bond=bond0 +''', + 'eth0.link': '''[Match] +MACAddress=00:01:02:03:04:05 +Type=!vlan bond bridge + +[Link] +Name=eth0 +WakeOnLan=off +''', + 'eth0.network': '''[Match] +MACAddress=00:01:02:03:04:05 +Name=eth0 +Type=!vlan bond bridge + +[Network] +LinkLocalAddressing=no +Bond=bond0 +''', + 'eth1.link': '''[Match] +MACAddress=02:01:02:03:04:05 +Type=!vlan bond bridge + +[Link] +Name=eth1 +WakeOnLan=off +''', + 'eth1.network': '''[Match] +MACAddress=02:01:02:03:04:05 +Name=eth1 +Type=!vlan bond bridge + +[Network] +LinkLocalAddressing=no +Bridge=br1 +'''}) + + +class TestMerging(TestBase): + '''multiple *.yaml merging''' + + def test_global_backend(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: y''', + confs={'backend': 'network:\n renderer: networkd'}) + + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:engreen,''') + self.assert_nm_udev(None) + + def test_add_def(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: true''', + confs={'blue': '''network: + version: 2 + ethernets: + enblue: + dhcp4: true'''}) + + self.assert_networkd({'enblue.network': ND_DHCP4 % 'enblue', + 'engreen.network': ND_DHCP4 % 'engreen'}) + # Skip on codecov.io; GLib changed hashtable elements ordering between + # releases, so we can't depend on the exact order. + # TODO: (cyphermox) turn this into an "assert_in_nm()" function. + if "CODECOV_TOKEN" not in os.environ: # pragma: nocover + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:engreen,interface-name:enblue,''') + self.assert_nm_udev(None) + + def test_change_def(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + wakeonlan: true + dhcp4: false''', + confs={'green-dhcp': '''network: + version: 2 + ethernets: + engreen: + dhcp4: true'''}) + + self.assert_networkd({'engreen.link': '[Match]\nOriginalName=engreen\n\n[Link]\nWakeOnLan=magic\n', + 'engreen.network': ND_DHCP4 % 'engreen'}) + + def test_cleanup_old_config(self): + self.generate('''network: + version: 2 + ethernets: + engreen: {dhcp4: true} + enyellow: {renderer: NetworkManager}''', + confs={'blue': '''network: + version: 2 + ethernets: + enblue: + dhcp4: true'''}) + + os.unlink(os.path.join(self.confdir, 'blue.yaml')) + self.generate('''network: + version: 2 + ethernets: + engreen: {dhcp4: true}''') + + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:engreen,''') + self.assert_nm_udev(None) + + def test_ref(self): + self.generate('''network: + version: 2 + ethernets: + eno1: {} + switchports: + match: + driver: yayroute''', + confs={'bridges': '''network: + version: 2 + bridges: + br0: + interfaces: [eno1, switchports] + dhcp4: true'''}, skip_generated_yaml_validation=True) + # XXX: We need to skip the generated YAML validation, as the 'bridges' + # conf is invalid in itself (missing eno1 & switchports defs) and + # can only be parsed if merged with the main YAML + + self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match] +Name=br0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes + +[DHCP] +RouteMetric=100 +UseMTU=true +''', + 'eno1.network': '[Match]\nName=eno1\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'switchports.network': '[Match]\nDriver=yayroute\n\n' + '[Network]\nLinkLocalAddressing=no\nBridge=br0\n'}) + + def test_def_in_run(self): + rundir = os.path.join(self.workdir.name, 'run', 'netplan') + os.makedirs(rundir) + # override b.yaml definition for enred + with open(os.path.join(rundir, 'b.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {enred: {dhcp4: true}}''') + + # append new definition for enblue + with open(os.path.join(rundir, 'c.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {enblue: {dhcp4: true}}''') + + self.generate('''network: + version: 2 + ethernets: + engreen: {dhcp4: true}''', confs={'b': '''network: + version: 2 + ethernets: {enred: {wakeonlan: true}}'''}) + + # b.yaml in /run/ should completely shadow b.yaml in /etc, thus no enred.link + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen', + 'enred.network': ND_DHCP4 % 'enred', + 'enblue.network': ND_DHCP4 % 'enblue'}) + + def test_def_in_lib(self): + libdir = os.path.join(self.workdir.name, 'lib', 'netplan') + rundir = os.path.join(self.workdir.name, 'run', 'netplan') + os.makedirs(libdir) + os.makedirs(rundir) + # b.yaml is in /etc/netplan too which should have precedence + with open(os.path.join(libdir, 'b.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {notme: {dhcp4: true}}''') + + # /run should trump /lib too + with open(os.path.join(libdir, 'c.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {alsonot: {dhcp4: true}}''') + with open(os.path.join(rundir, 'c.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {enyellow: {dhcp4: true}}''') + + # this should be considered + with open(os.path.join(libdir, 'd.yaml'), 'w') as f: + f.write('''network: + version: 2 + ethernets: {enblue: {dhcp4: true}}''') + + self.generate('''network: + version: 2 + ethernets: + engreen: {dhcp4: true}''', confs={'b': '''network: + version: 2 + ethernets: {enred: {wakeonlan: true}}'''}) + + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen', + 'enred.link': '[Match]\nOriginalName=enred\n\n[Link]\nWakeOnLan=magic\n', + 'enred.network': '''[Match] +Name=enred + +[Network] +LinkLocalAddressing=ipv6 +''', + 'enyellow.network': ND_DHCP4 % 'enyellow', + 'enblue.network': ND_DHCP4 % 'enblue'}) diff --git a/tests/generator/test_dhcp_overrides.py b/tests/generator/test_dhcp_overrides.py new file mode 100644 index 0000000..7d5bb61 --- /dev/null +++ b/tests/generator/test_dhcp_overrides.py @@ -0,0 +1,426 @@ +# +# Tests for DHCP override handling in netplan generator +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import (TestBase, ND_DHCP4, ND_DHCP4_NOMTU, ND_DHCP6, + ND_DHCP6_NOMTU, ND_DHCPYES, ND_DHCPYES_NOMTU) + + +class TestNetworkd(TestBase): + + # Common tests for dhcp override booleans + def assert_dhcp_overrides_bool(self, override_name, networkd_name): + # dhcp4 yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes +''' % override_name) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + + # dhcp6 yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: yes +''' % override_name) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen'}) + + # dhcp4 and dhcp6 both yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes + dhcp6: yes + dhcp6-overrides: + %s: yes +''' % (override_name, override_name)) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen'}) + + # dhcp4 no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: no +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen' + '%s=false\n' % networkd_name}) + + # dhcp6 no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: no +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen' + '%s=false\n' % networkd_name}) + + # dhcp4 and dhcp6 both no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: no + dhcp6: yes + dhcp6-overrides: + %s: no +''' % (override_name, override_name)) + self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen' + '%s=false\n' % networkd_name}) + + # mismatched values + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes + dhcp6: yes + dhcp6-overrides: + %s: no +''' % (override_name, override_name), expect_fail=True) + self.assertEqual(err, 'ERROR: engreen: networkd requires that ' + '%s has the same value in both dhcp4_overrides and dhcp6_overrides\n' % override_name) + + # Common tests for dhcp override strings + def assert_dhcp_overrides_string(self, override_name, networkd_name): + # dhcp4 only + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: foo +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen' + '%s=foo\n' % networkd_name}) + + # dhcp6 only + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: foo +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen' + '%s=foo\n' % networkd_name}) + + # dhcp4 and dhcp6 + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: foo + dhcp6: yes + dhcp6-overrides: + %s: foo +''' % (override_name, override_name)) + self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen' + '%s=foo\n' % networkd_name}) + + # mismatched values + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: foo + dhcp6: yes + dhcp6-overrides: + %s: bar +''' % (override_name, override_name), expect_fail=True) + self.assertEqual(err, 'ERROR: engreen: networkd requires that ' + '%s has the same value in both dhcp4_overrides and dhcp6_overrides\n' % override_name) + + # Common tests for dhcp override booleans + def assert_dhcp_mtu_overrides_bool(self, override_name, networkd_name): + # dhcp4 yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + + # dhcp6 yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: yes +''' % override_name) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCP6 % 'engreen'}) + + # dhcp4 and dhcp6 both yes + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes + dhcp6: yes + dhcp6-overrides: + %s: yes +''' % (override_name, override_name)) + # silently ignored since yes is the default + self.assert_networkd({'engreen.network': ND_DHCPYES % 'engreen'}) + + # dhcp4 no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: no +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP4_NOMTU % 'engreen'}) + + # dhcp6 no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: no +''' % override_name) + self.assert_networkd({'engreen.network': ND_DHCP6_NOMTU % 'engreen'}) + + # dhcp4 and dhcp6 both no + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: no + dhcp6: yes + dhcp6-overrides: + %s: no +''' % (override_name, override_name)) + self.assert_networkd({'engreen.network': ND_DHCPYES_NOMTU % 'engreen'}) + + # mismatched values + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: yes + dhcp6: yes + dhcp6-overrides: + %s: no +''' % (override_name, override_name), expect_fail=True) + self.assertEqual(err, 'ERROR: engreen: networkd requires that ' + '%s has the same value in both dhcp4_overrides and dhcp6_overrides\n' % override_name) + + def assert_dhcp_overrides_guint(self, override_name, networkd_name): + # dhcp4 only + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: 6000 +''' % override_name) + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=6000 +UseMTU=true +'''}) + + # dhcp6 only + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp6: yes + dhcp6-overrides: + %s: 6000 +''' % override_name) + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv6 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=6000 +UseMTU=true +'''}) + + # dhcp4 and dhcp6 + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: 6000 + dhcp6: yes + dhcp6-overrides: + %s: 6000 +''' % (override_name, override_name)) + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=6000 +UseMTU=true +'''}) + + # mismatched values + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + %s: 3333 + dhcp6: yes + dhcp6-overrides: + %s: 5555 +''' % (override_name, override_name), expect_fail=True) + self.assertEqual(err, 'ERROR: engreen: networkd requires that ' + '%s has the same value in both dhcp4_overrides and dhcp6_overrides\n' % override_name) + + def test_dhcp_overrides_use_dns(self): + self.assert_dhcp_overrides_bool('use-dns', 'UseDNS') + + def test_dhcp_overrides_use_domains(self): + self.assert_dhcp_overrides_string('use-domains', 'UseDomains') + + def test_dhcp_overrides_use_ntp(self): + self.assert_dhcp_overrides_bool('use-ntp', 'UseNTP') + + def test_dhcp_overrides_send_hostname(self): + self.assert_dhcp_overrides_bool('send-hostname', 'SendHostname') + + def test_dhcp_overrides_use_hostname(self): + self.assert_dhcp_overrides_bool('use-hostname', 'UseHostname') + + def test_dhcp_overrides_hostname(self): + self.assert_dhcp_overrides_string('hostname', 'Hostname') + + def test_dhcp_overrides_use_mtu(self): + self.assert_dhcp_mtu_overrides_bool('use-mtu', 'UseMTU') + + def test_dhcp_overrides_default_metric(self): + self.assert_dhcp_overrides_guint('route-metric', 'RouteMetric') + + def test_dhcp_overrides_use_routes(self): + self.assert_dhcp_overrides_bool('use-routes', 'UseRoutes') + + +class TestNetworkManager(TestBase): + + def test_override_default_metric_v4(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: yes + dhcp4-overrides: + route-metric: 3333 +''') + # silently ignored since yes is the default + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto +route-metric=3333 + +[ipv6] +method=ignore +'''}) + + def test_override_default_metric_v6(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + dhcp6-overrides: + route-metric: 6666 +''') + # silently ignored since yes is the default + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=auto +route-metric=6666 +'''}) diff --git a/tests/generator/test_errors.py b/tests/generator/test_errors.py new file mode 100644 index 0000000..da91e4b --- /dev/null +++ b/tests/generator/test_errors.py @@ -0,0 +1,976 @@ +# +# Tests for common invalid syntax/errors in config +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import TestBase + + +class TestConfigErrors(TestBase): + def test_malformed_yaml(self): + err = self.generate('network:\n version: %&', expect_fail=True) + self.assertIn('Invalid YAML', err) + self.assertIn('found character that cannot start any token', err) + + def test_wrong_indent(self): + err = self.generate('network:\n version: 2\n foo: *', expect_fail=True) + self.assertIn('Invalid YAML', err) + self.assertIn('inconsistent indentation', err) + + def test_yaml_expected_scalar(self): + err = self.generate('network:\n version: {}', expect_fail=True) + self.assertIn('expected scalar', err) + + def test_yaml_expected_sequence(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + interfaces: {}''', expect_fail=True) + self.assertIn('expected sequence', err) + + def test_yaml_expected_mapping(self): + err = self.generate('network:\n version', expect_fail=True) + self.assertIn('expected mapping', err) + + def test_invalid_bool(self): + err = self.generate('''network: + version: 2 + ethernets: + id0: + wakeonlan: wut +''', expect_fail=True) + self.assertIn("invalid boolean value 'wut'", err) + + def test_invalid_version(self): + err = self.generate('network:\n version: 1', expect_fail=True) + self.assertIn('Only version 2 is supported', err) + + def test_id_redef_type_mismatch(self): + err = self.generate('''network: + version: 2 + ethernets: + id0: + wakeonlan: true''', + confs={'redef': '''network: + version: 2 + bridges: + id0: + wakeonlan: true'''}, expect_fail=True) + self.assertIn("Updated definition 'id0' changes device type", err) + + def test_set_name_without_match(self): + err = self.generate('''network: + version: 2 + ethernets: + def1: + set-name: lom1 +''', expect_fail=True) + self.assertIn("def1: 'set-name:' requires 'match:' properties", err) + + def test_virtual_set_name(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + set_name: br1''', expect_fail=True) + self.assertIn("unknown key 'set_name'", err) + + def test_virtual_match(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + match: + driver: foo''', expect_fail=True) + self.assertIn("unknown key 'match'", err) + + def test_virtual_wol(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + wakeonlan: true''', expect_fail=True) + self.assertIn("unknown key 'wakeonlan'", err) + + def test_unknown_global_renderer(self): + err = self.generate('''network: + version: 2 + renderer: bogus +''', expect_fail=True) + self.assertIn("unknown renderer 'bogus'", err) + + def test_unknown_type_renderer(self): + err = self.generate('''network: + version: 2 + ethernets: + renderer: bogus +''', expect_fail=True) + self.assertIn("unknown renderer 'bogus'", err) + + def test_invalid_id(self): + err = self.generate('''network: + version: 2 + ethernets: + "eth 0": + dhcp4: true''', expect_fail=True) + self.assertIn("Invalid name 'eth 0'", err) + + def test_invalid_name_match(self): + err = self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: | + fo o + bar + dhcp4: true''', expect_fail=True) + self.assertIn("Invalid name 'fo o\nbar\n'", err) + + def test_invalid_mac_match(self): + err = self.generate('''network: + version: 2 + ethernets: + def1: + match: + macaddress: 00:11:ZZ + dhcp4: true''', expect_fail=True) + self.assertIn("Invalid MAC address '00:11:ZZ', must be XX:XX:XX:XX:XX:XX", err) + + def test_glob_in_id(self): + err = self.generate('''network: + version: 2 + ethernets: + en*: + dhcp4: true''', expect_fail=True) + self.assertIn("Definition ID 'en*' must not use globbing", err) + + def test_wifi_duplicate_ssid(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + password: "s3kr1t" + workplace: + password: "c0mpany" + dhcp4: yes''', expect_fail=True) + self.assertIn("wl0: Duplicate access point SSID 'workplace'", err) + + def test_wifi_no_ap(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + dhcp4: yes''', expect_fail=True) + self.assertIn('wl0: No access points defined', err) + + def test_wifi_empty_ap(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: {} + dhcp4: yes''', expect_fail=True) + self.assertIn('wl0: No access points defined', err) + + def test_wifi_ap_unknown_key(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + something: false + dhcp4: yes''', expect_fail=True) + self.assertIn("unknown key 'something'", err) + + def test_wifi_ap_unknown_mode(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + mode: bogus''', expect_fail=True) + self.assertIn("unknown wifi mode 'bogus'", err) + + def test_wifi_ap_unknown_band(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + band: bogus''', expect_fail=True) + self.assertIn("unknown wifi band 'bogus'", err) + + def test_wifi_ap_invalid_freq24(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + workplace: + band: 2.4GHz + channel: 15''', expect_fail=True) + self.assertIn("ERROR: invalid 2.4GHz WiFi channel: 15", err) + + def test_wifi_ap_invalid_freq5(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + band: 5GHz + channel: 14''', expect_fail=True) + self.assertIn("ERROR: invalid 5GHz WiFi channel: 14", err) + + def test_wifi_invalid_hidden(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + hidden: + hidden: maybe''', expect_fail=True) + self.assertIn("invalid boolean value 'maybe'", err) + + def test_invalid_ipv4_address(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14/24 + - 2001:FFfe::1/64''', expect_fail=True) + + self.assertIn("malformed address '192.168.14/24', must be X.X.X.X/NN", err) + + def test_missing_ipv4_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.1''', expect_fail=True) + + self.assertIn("address '192.168.14.1' is missing /prefixlength", err) + + def test_empty_ipv4_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.1/''', expect_fail=True) + + self.assertIn("invalid prefix length in address '192.168.14.1/'", err) + + def test_invalid_ipv4_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.14.1/33''', expect_fail=True) + + self.assertIn("invalid prefix length in address '192.168.14.1/33'", err) + + def test_invalid_ipv6_address(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 2001:G::1/64''', expect_fail=True) + + self.assertIn("malformed address '2001:G::1/64', must be X.X.X.X/NN", err) + + def test_missing_ipv6_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 2001::1''', expect_fail=True) + self.assertIn("address '2001::1' is missing /prefixlength", err) + + def test_invalid_ipv6_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 2001::1/129''', expect_fail=True) + self.assertIn("invalid prefix length in address '2001::1/129'", err) + + def test_empty_ipv6_prefixlen(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 2001::1/''', expect_fail=True) + self.assertIn("invalid prefix length in address '2001::1/'", err) + + def test_invalid_addr_gen_mode(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + ipv6-address-generation: 0''', expect_fail=True) + self.assertIn("unknown ipv6-address-generation '0'", err) + + def test_addr_gen_mode_not_supported(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + ipv6-address-generation: stable-privacy''', expect_fail=True) + self.assertIn("ERROR: engreen: ipv6-address-generation mode is not supported by networkd", err) + + def test_addr_gen_mode_and_addr_gen_token(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + ipv6-address-token: "::2" + ipv6-address-generation: eui64''', expect_fail=True) + self.assertIn("engreen: ipv6-address-generation and ipv6-address-token are mutually exclusive", err) + + def test_invalid_addr_gen_token(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + ipv6-address-token: INVALID''', expect_fail=True) + self.assertIn("invalid ipv6-address-token 'INVALID'", err) + + def test_nm_devices_missing_passthrough(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + nm-devices: + engreen: + networkmanager: + passthrough: + connection.uuid: "123456"''', expect_fail=True) + self.assertIn("engreen: network type 'nm-devices:' needs to provide a 'connection.type' via passthrough", err) + + def test_invalid_address_node_type(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: [[192.168.1.15]]''', expect_fail=True) + self.assertIn("expected either scalar or mapping (check indentation)", err) + + def test_invalid_address_option_value(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 0.0.0.0.0/24: + lifetime: 0''', expect_fail=True) + self.assertIn("malformed address '0.0.0.0.0/24', must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", err) + + def test_invalid_address_option_lifetime(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: + - 192.168.1.15/24: + lifetime: 1''', expect_fail=True) + self.assertIn("invalid lifetime value '1'", err) + + def test_invalid_nm_options(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: + - 192.168.1.15/24: + lifetime: 0''', expect_fail=True) + self.assertIn('NetworkManager does not support address options', err) + + def test_invalid_gateway4(self): + for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + gateway4: %s''' % a, expect_fail=True) + self.assertIn("invalid IPv4 address '%s'" % a, err) + + def test_invalid_gateway6(self): + for a in ['1234', '1:::c', '1234::1/50']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + gateway6: %s''' % a, expect_fail=True) + self.assertIn("invalid IPv6 address '%s'" % a, err) + + def test_multiple_ip4_gateways(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: [192.168.22.78/24] + gateway4: 192.168.22.1 + enblue: + addresses: [10.49.34.4/16] + gateway4: 10.49.2.38''', expect_fail=False) + self.assertIn("Problem encountered while validating default route consistency", err) + self.assertIn("Conflicting default route declarations for IPv4 (table: main, metric: default)", err) + self.assertIn("engreen", err) + self.assertIn("enblue", err) + + def test_multiple_ip6_gateways(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: [2001:FFfe::1/62] + gateway6: 2001:FFfe::2 + enblue: + addresses: [2001:FFfe::33/62] + gateway6: 2001:FFfe::34''', expect_fail=False) + self.assertIn("Problem encountered while validating default route consistency", err) + self.assertIn("Conflicting default route declarations for IPv6 (table: main, metric: default)", err) + self.assertIn("engreen", err) + self.assertIn("enblue", err) + + def test_gateway_and_default_route(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: [10.49.34.4/16] + gateway4: 10.49.2.38 + routes: + - to: default + via: 10.49.65.89''', expect_fail=False) + self.assertIn("Problem encountered while validating default route consistency", err) + self.assertIn("Conflicting default route declarations for IPv4 (table: main, metric: default)", err) + self.assertIn("engreen", err) + + def test_multiple_default_routes_on_other_table(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: [10.49.34.4/16] + routes: + - to: default + via: 10.49.65.89 + enblue: + addresses: [10.50.35.3/16] + routes: + - to: default + via: 10.49.65.89 + table: 23 + enred: + addresses: [172.137.1.4/24] + routes: + - to: default + via: 172.137.1.1 + table: 23 + ''', expect_fail=False) + self.assertIn("Problem encountered while validating default route consistency", err) + self.assertIn("Conflicting default route declarations for IPv4 (table: 23, metric: default)", err) + self.assertIn("enblue", err) + self.assertIn("enred", err) + self.assertNotIn("engreen", err) + + def test_multiple_default_routes_on_specific_metrics(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: [10.49.34.4/16] + routes: + - to: default + via: 10.49.65.89 + metric: 100 + enblue: + addresses: [10.50.35.3/16] + routes: + - to: default + via: 10.49.65.89 + metric: 600 + enred: + addresses: [172.137.1.4/24] + routes: + - to: default + via: 172.137.1.1 + metric: 600 + ''', expect_fail=False) + self.assertIn("Problem encountered while validating default route consistency", err) + self.assertIn("Conflicting default route declarations for IPv4 (table: main, metric: 600)", err) + self.assertIn("enblue", err) + self.assertIn("enred", err) + self.assertNotIn("engreen", err) + + def test_default_route_and_0(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: [10.49.34.4/16] + routes: + - to: default + via: 10.49.65.89 + - to: 0.0.0.0/0 + via: 10.49.65.67''', expect_fail=False) + self.assertIn("Problem encountered while validating default route consistency", err) + self.assertIn("Conflicting default route declarations for IPv4 (table: main, metric: default)", err) + self.assertIn("engreen", err) + + def test_invalid_nameserver_ipv4(self): + for a in ['300.400.1.1', '1.2.3', '192.168.14.1/24']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + nameservers: + addresses: [%s]''' % a, expect_fail=True) + self.assertIn("malformed address '%s'" % a, err) + + def test_invalid_nameserver_ipv6(self): + for a in ['1234', '1:::c', '1234::1/50']: + err = self.generate('''network: + version: 2 + ethernets: + engreen: + nameservers: + addresses: ["%s"]''' % a, expect_fail=True) + self.assertIn("malformed address '%s'" % a, err) + + def test_vlan_missing_id(self): + err = self.generate('''network: + version: 2 + ethernets: {en1: {}} + vlans: + ena: {link: en1}''', expect_fail=True) + self.assertIn("missing 'id' property", err) + + def test_vlan_invalid_id(self): + err = self.generate('''network: + version: 2 + ethernets: {en1: {}} + vlans: + ena: {id: a, link: en1}''', expect_fail=True) + self.assertIn("invalid unsigned int value 'a'", err) + + err = self.generate('''network: + version: 2 + ethernets: {en1: {}} + vlans: + ena: {id: 4095, link: en1}''', expect_fail=True) + self.assertIn("invalid id '4095'", err) + + def test_vlan_missing_link(self): + err = self.generate('''network: + version: 2 + vlans: + ena: {id: 1}''', expect_fail=True) + self.assertIn("ena: missing 'link' property", err) + + def test_vlan_unknown_link(self): + err = self.generate('''network: + version: 2 + vlans: + ena: {id: 1, link: en1}''', expect_fail=True) + self.assertIn("ena: interface 'en1' is not defined", err) + + def test_vlan_unknown_renderer(self): + err = self.generate('''network: + version: 2 + ethernets: {en1: {}} + vlans: + ena: {id: 1, link: en1, renderer: foo}''', expect_fail=True) + self.assertIn("unknown renderer 'foo'", err) + + def test_device_bad_route_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: badlocation + via: 192.168.14.20 + metric: 100 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_bad_route_via(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: badgateway + metric: 100 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_bad_route_metric(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: 10.1.1.1 + metric: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_bad_route_mtu(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: 10.1.1.1 + mtu: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + self.assertIn("invalid unsigned int value '-1'", err) + + def test_device_bad_route_congestion_window(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: 10.1.1.1 + congestion-window: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + self.assertIn("invalid unsigned int value '-1'", err) + + def test_device_bad_route_advertised_receive_window(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: 10.1.1.1 + advertised-receive-window: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + self.assertIn("invalid unsigned int value '-1'", err) + + def test_device_route_family_mismatch_ipv6_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 2001:dead:beef::0/16 + via: 10.1.1.1 + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_family_mismatch_ipv4_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 10.10.10.0/24 + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_missing_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_missing_via(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 2001:dead:beef::2 + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_type_missing_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + type: prohibit + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_scope_link_missing_to(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + scope: link + metric: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_invalid_onlink(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 2000:cafe:cafe::1/24 + on-link: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_invalid_table(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 2000:cafe:cafe::1/24 + table: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_invalid_type(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 2000:cafe:cafe::1/24 + type: thisisinvalidtype + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_route_invalid_scope(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - via: 2001:dead:beef::2 + to: 2000:cafe:cafe::1/24 + scope: linky + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_mismatched_addresses(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + to: 2000:dead:beef::3/64 + table: 50 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_missing_address(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - table: 50 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_tos(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + type-of-service: 256 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_prio(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + priority: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_table(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + table: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_fwmark(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - from: 10.10.10.0/24 + mark: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_device_ip_rule_invalid_address(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + routing-policy: + - to: 10.10.10.0/24 + from: someinvalidaddress + mark: 1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + def test_invalid_dhcp_identifier(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp-identifier: invalid''', expect_fail=True) + + def test_invalid_accept_ra(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + accept-ra: j''', expect_fail=True) + self.assertIn('invalid boolean', err) + + def test_invalid_link_local_set(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: invalid''', expect_fail=True) + + def test_invalid_link_local_value(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: yes + dhcp6: yes + link-local: [ invalid, ]''', expect_fail=True) + + def test_invalid_yaml_tabs(self): + err = self.generate('''\t''', expect_fail=True) + self.assertIn("tabs are not allowed for indent", err) + + def test_invalid_yaml_undefined_alias(self): + err = self.generate('''network: + version: 2 + ethernets: + *engreen: + dhcp4: yes''', expect_fail=True) + self.assertIn("aliases are not supported", err) + + def test_invalid_yaml_undefined_alias_at_eof(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: *yes''', expect_fail=True) + self.assertIn("aliases are not supported", err) + + def test_invalid_activation_mode(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + activation-mode: invalid''', expect_fail=True) + self.assertIn("needs to be 'manual' or 'off'", err) diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py new file mode 100644 index 0000000..ac8ffc8 --- /dev/null +++ b/tests/generator/test_ethernets.py @@ -0,0 +1,715 @@ +# +# Tests for ethernet devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from .base import TestBase, ND_DHCP4, UDEV_MAC_RULE, UDEV_NO_MAC_RULE, UDEV_SRIOV_RULE + + +class TestNetworkd(TestBase): + + def test_eth_wol(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + wakeonlan: true + dhcp4: n''') + + self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=magic\n', + 'eth0.network': '''[Match] +Name=eth0 + +[Network] +LinkLocalAddressing=ipv6 +'''}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:eth0,''') + self.assert_nm_udev(None) + # should not allow NM to manage everything + self.assertFalse(os.path.exists(self.nm_enable_all_conf)) + + def test_eth_lldp(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp4: n + emit-lldp: true''') + + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +EmitLLDP=true +LinkLocalAddressing=ipv6 +'''}) + + def test_eth_mtu(self): + self.generate('''network: + version: 2 + ethernets: + eth1: + mtu: 1280 + dhcp4: n''') + + self.assert_networkd({'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n', + 'eth1.network': '''[Match] +Name=eth1 + +[Link] +MTUBytes=1280 + +[Network] +LinkLocalAddressing=ipv6 +'''}) + self.assert_networkd_udev(None) + + def test_eth_sriov_vlan_filterv_link(self): + self.generate('''network: + version: 2 + ethernets: + enp1: + dhcp4: n + enp1s16f1: + dhcp4: n + link: enp1''') + + self.assert_networkd({'enp1.network': '''[Match] +Name=enp1 + +[Network] +LinkLocalAddressing=ipv6 +''', + 'enp1s16f1.network': '''[Match] +Name=enp1s16f1 + +[Network] +LinkLocalAddressing=ipv6 +'''}) + self.assert_additional_udev({'99-sriov-netplan-setup.rules': UDEV_SRIOV_RULE}) + + def test_eth_sriov_virtual_functions(self): + self.generate('''network: + version: 2 + ethernets: + enp1: + virtual-function-count: 8''') + + self.assert_networkd({'enp1.network': '''[Match] +Name=enp1 + +[Network] +LinkLocalAddressing=ipv6 +'''}) + self.assert_additional_udev({'99-sriov-netplan-setup.rules': UDEV_SRIOV_RULE}) + + def test_eth_match_by_driver_rename(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + driver: ixgbe + set-name: lom1''') + + self.assert_networkd({'def1.link': '[Match]\nDriver=ixgbe\n\n[Link]\nName=lom1\nWakeOnLan=off\n', + 'def1.network': '''[Match] +Driver=ixgbe +Name=lom1 + +[Network] +LinkLocalAddressing=ipv6 +'''}) + self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('ixgbe', 'lom1'))}) + # NM cannot match by driver, so blacklisting needs to happen via udev + self.assert_nm(None, None) + self.assert_nm_udev('ACTION=="add|change", SUBSYSTEM=="net", ENV{ID_NET_DRIVER}=="ixgbe", ENV{NM_UNMANAGED}="1"\n') + + def test_eth_match_by_mac_rename(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + macaddress: 11:22:33:44:55:66 + set-name: lom1''') + + self.assert_networkd({'def1.link': '[Match]\nMACAddress=11:22:33:44:55:66\n\n[Link]\nName=lom1\nWakeOnLan=off\n', + 'def1.network': '''[Match] +MACAddress=11:22:33:44:55:66 +Name=lom1 + +[Network] +LinkLocalAddressing=ipv6 +'''}) + self.assert_networkd_udev({'def1.rules': (UDEV_MAC_RULE % ('?*', '11:22:33:44:55:66', 'lom1'))}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=mac:11:22:33:44:55:66,interface-name:lom1,''') + self.assert_nm_udev(None) + + def test_eth_implicit_name_match_dhcp4(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: y''') + + self.assert_networkd({'engreen.network': ND_DHCP4 % 'engreen'}) + self.assert_networkd_udev(None) + + def test_eth_match_dhcp4(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + driver: ixgbe + dhcp4: true''') + + self.assert_networkd({'def1.network': '''[Match] +Driver=ixgbe + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_networkd_udev(None) + self.assert_nm_udev('ACTION=="add|change", SUBSYSTEM=="net", ENV{ID_NET_DRIVER}=="ixgbe", ENV{NM_UNMANAGED}="1"\n') + + def test_eth_match_name(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: green + dhcp4: true''') + + self.assert_networkd({'def1.network': ND_DHCP4 % 'green'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:green,''') + self.assert_nm_udev(None) + + def test_eth_set_mac(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: green + macaddress: 00:01:02:03:04:05 + dhcp4: true''') + + self.assert_networkd({'def1.network': (ND_DHCP4 % 'green') + .replace('[Network]', '[Link]\nMACAddress=00:01:02:03:04:05\n\n[Network]') + }) + self.assert_networkd_udev(None) + + def test_eth_match_name_rename(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: green + set-name: blue + dhcp4: true''') + + # the .network needs to match on the renamed name + self.assert_networkd({'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nName=blue\nWakeOnLan=off\n', + 'def1.network': ND_DHCP4 % 'blue'}) + + # The udev rules engine does support renaming by name + self.assert_networkd_udev(None) + + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:blue,''') + + def test_eth_match_all_names(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: {name: "*"} + dhcp4: true''') + + self.assert_networkd({'def1.network': ND_DHCP4 % '*'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:*,''') + self.assert_nm_udev(None) + + def test_eth_match_all(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: {} + dhcp4: true''') + + self.assert_networkd({'def1.network': '[Match]\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n' + '[DHCP]\nRouteMetric=100\nUseMTU=true\n'}) + self.assert_networkd_udev(None) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=type:ethernet,''') + self.assert_nm_udev(None) + + def test_match_multiple(self): + self.generate('''network: + version: 2 + ethernets: + def1: + match: + name: en1s* + macaddress: 00:11:22:33:44:55 + dhcp4: on''') + self.assert_networkd({'def1.network': '''[Match] +MACAddress=00:11:22:33:44:55 +Name=en1s* + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=mac:00:11:22:33:44:55,interface-name:en1s*,''') + + +class TestNetworkManager(TestBase): + + def test_eth_wol(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + wakeonlan: true''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=1 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + # should allow NM to manage everything else + self.assertTrue(os.path.exists(self.nm_enable_all_conf)) + self.assert_networkd({'eth0.link': '[Match]\nOriginalName=eth0\n\n[Link]\nWakeOnLan=magic\n'}) + self.assert_nm_udev(None) + + def test_eth_mtu(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth1: + mtu: 1280 + dhcp4: n''') + + self.assert_networkd({'eth1.link': '[Match]\nOriginalName=eth1\n\n[Link]\nWakeOnLan=off\nMTUBytes=1280\n'}) + self.assert_nm({'eth1': '''[connection] +id=netplan-eth1 +type=ethernet +interface-name=eth1 + +[ethernet] +wake-on-lan=0 +mtu=1280 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_eth_sriov_link(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + enp1: + dhcp4: n + enp1s16f1: + dhcp4: n + link: enp1''') + + self.assert_networkd({}) + self.assert_nm({'enp1': '''[connection] +id=netplan-enp1 +type=ethernet +interface-name=enp1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'enp1s16f1': '''[connection] +id=netplan-enp1s16f1 +type=ethernet +interface-name=enp1s16f1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_additional_udev({'99-sriov-netplan-setup.rules': UDEV_SRIOV_RULE}) + + def test_eth_sriov_virtual_functions(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + enp1: + dhcp4: n + virtual-function-count: 8''') + + self.assert_networkd({}) + self.assert_nm({'enp1': '''[connection] +id=netplan-enp1 +type=ethernet +interface-name=enp1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_additional_udev({'99-sriov-netplan-setup.rules': UDEV_SRIOV_RULE}) + + def test_eth_set_mac(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + macaddress: 00:01:02:03:04:05 + dhcp4: true''') + + self.assert_networkd(None) + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 +cloned-mac-address=00:01:02:03:04:05 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + + def test_eth_match_by_driver(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + driver: ixgbe''', expect_fail=True) + self.assertIn('NetworkManager definitions do not support matching by driver', err) + + def test_eth_match_by_driver_rename(self): + # in this case udev will rename the device so that NM can use the name + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + driver: ixgbe + set-name: lom1''') + + self.assert_networkd({'def1.link': '[Match]\nDriver=ixgbe\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) + self.assert_networkd_udev({'def1.rules': (UDEV_NO_MAC_RULE % ('ixgbe', 'lom1'))}) + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=lom1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_nm_udev(None) + + def test_eth_match_by_mac_rename(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + macaddress: 11:22:33:44:55:66 + set-name: lom1''') + + self.assert_networkd({'def1.link': '[Match]\nMACAddress=11:22:33:44:55:66\n\n[Link]\nName=lom1\nWakeOnLan=off\n'}) + self.assert_networkd_udev({'def1.rules': (UDEV_MAC_RULE % ('?*', '11:22:33:44:55:66', 'lom1'))}) + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=lom1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_nm_udev(None) + + def test_eth_implicit_name_match_dhcp4(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_eth_match_mac_dhcp4(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + macaddress: 11:22:33:44:55:66 + dhcp4: true''') + + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet + +[ethernet] +wake-on-lan=0 +mac-address=11:22:33:44:55:66 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_eth_match_name(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + name: green + dhcp4: true''') + + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=green + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_eth_match_name_rename(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + name: green + set-name: blue + dhcp4: true''') + + # The udev rules engine does support renaming by name + self.assert_networkd_udev(None) + + # NM needs to match on the renamed name + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=blue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + # ... while udev renames it + self.assert_networkd({'def1.link': '[Match]\nOriginalName=green\n\n[Link]\nName=blue\nWakeOnLan=off\n'}) + self.assert_nm_udev(None) + + def test_eth_match_name_glob(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: {name: "en*"} + dhcp4: true''') + + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet + +[ethernet] +wake-on-lan=0 + +[match] +interface-name=en*; + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_eth_match_all(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: {} + dhcp4: true''') + + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_match_multiple(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + def1: + match: + name: engreen + macaddress: 00:11:22:33:44:55 + dhcp4: yes''') + self.assert_nm({'def1': '''[connection] +id=netplan-def1 +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 +mac-address=00:11:22:33:44:55 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) diff --git a/tests/generator/test_modems.py b/tests/generator/test_modems.py new file mode 100644 index 0000000..acffe87 --- /dev/null +++ b/tests/generator/test_modems.py @@ -0,0 +1,426 @@ +# +# Tests for gsm/cdma modem devices config generated via netplan +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import TestBase + + +class TestNetworkd(TestBase): + '''networkd output''' + + def test_not_supported(self): + # does not produce any output, but fails with: + # "networkd backend does not support GSM modem configuration" + err = self.generate('''network: + version: 2 + modems: + mobilephone: + auto-config: true''', expect_fail=True) + self.assertIn("ERROR: mobilephone: networkd backend does not support GSM/CDMA modem configuration", err) + + self.assert_networkd({}) + self.assert_nm({}) + + +class TestNetworkManager(TestBase): + '''networkmanager output''' + + def test_cdma_config(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + mtu: 1542 + number: "#666" + username: test-user + password: s0s3kr1t''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=cdma +interface-name=mobilephone + +[cdma] +password=s0s3kr1t +username=test-user +mtu=1542 +number=#666 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_auto_config(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + auto-config: true''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_auto_config_implicit(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + number: "*99#" + mtu: 1600 + pin: "1234"''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true +mtu=1600 +number=*99# +pin=1234 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_apn(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + apn: internet''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +apn=internet + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_apn_username_password(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + apn: internet + username: some-user + password: some-pass''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +apn=internet +password=some-pass +username=some-user + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_device_id(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + device-id: test''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true +device-id=test + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_network_id(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + network-id: test''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true +network-id=test + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_pin(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + pin: 1234''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true +pin=1234 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_sim_id(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + sim-id: test''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true +sim-id=test + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_sim_operator_id(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + sim-operator-id: test''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +interface-name=mobilephone + +[gsm] +auto-config=true +sim-operator-id=test + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_gsm_example(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + cdc-wdm1: + mtu: 1600 + apn: ISP.CINGULAR + username: ISP@CINGULARGPRS.COM + password: CINGULAR1 + number: "*99#" + network-id: 24005 + device-id: da812de91eec16620b06cd0ca5cbc7ea25245222 + pin: 2345 + sim-id: 89148000000060671234 + sim-operator-id: 310260''') + self.assert_nm({'cdc-wdm1': '''[connection] +id=netplan-cdc-wdm1 +type=gsm +interface-name=cdc-wdm1 + +[gsm] +apn=ISP.CINGULAR +password=CINGULAR1 +username=ISP@CINGULARGPRS.COM +device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 +mtu=1600 +network-id=24005 +number=*99# +pin=2345 +sim-id=89148000000060671234 +sim-operator-id=310260 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_modem_nm_integration(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + modems: + mobilephone: + auto-config: true + networkmanager: + uuid: b22d8f0f-3f34-46bd-ac28-801fa87f1eb6''') + self.assert_nm({'mobilephone': '''[connection] +id=netplan-mobilephone +type=gsm +uuid=b22d8f0f-3f34-46bd-ac28-801fa87f1eb6 +interface-name=mobilephone + +[gsm] +auto-config=true + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_modem_nm_integration_gsm_cdma(self): + self.generate('''network: + version: 2 + modems: + NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3: + renderer: NetworkManager + match: {} + apn: internet2.voicestream.com + networkmanager: + uuid: a08c5805-7cf5-43f7-afb9-12cb30f6eca3 + name: "T-Mobile Funkadelic 2" + passthrough: + connection.type: "bluetooth" + gsm.apn: "internet2.voicestream.com" + gsm.device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" + gsm.username: "george.clinton.again" + gsm.sim-operator-id: "310260" + gsm.pin: "123456" + gsm.sim-id: "89148000000060671234" + gsm.password: "parliament2" + gsm.network-id: "254098" + ipv4.method: "auto" + ipv6.method: "auto"''') + self.assert_nm({'NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3': '''[connection] +id=T-Mobile Funkadelic 2 +#Netplan: passthrough override +type=bluetooth +uuid=a08c5805-7cf5-43f7-afb9-12cb30f6eca3 + +[gsm] +apn=internet2.voicestream.com +#Netplan: passthrough setting +device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 +#Netplan: passthrough setting +username=george.clinton.again +#Netplan: passthrough setting +sim-operator-id=310260 +#Netplan: passthrough setting +pin=123456 +#Netplan: passthrough setting +sim-id=89148000000060671234 +#Netplan: passthrough setting +password=parliament2 +#Netplan: passthrough setting +network-id=254098 + +[ipv4] +#Netplan: passthrough override +method=auto + +[ipv6] +#Netplan: passthrough override +method=auto +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py new file mode 100644 index 0000000..e7084a9 --- /dev/null +++ b/tests/generator/test_ovs.py @@ -0,0 +1,1021 @@ +# +# Common tests for netplan OpenVSwitch support +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Łukasz 'sil2100' Zemczak +# Lukas 'slyon' Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import TestBase, ND_EMPTY, ND_WITHIP, ND_DHCP4, ND_DHCP6, \ + OVS_PHYSICAL, OVS_VIRTUAL, \ + OVS_BR_EMPTY, OVS_BR_DEFAULT, \ + OVS_CLEANUP + + +class TestOpenVSwitch(TestBase): + '''OVS output''' + + def test_interface_external_ids_other_config(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + openvswitch: + external-ids: + iface-id: myhostname + other-config: + disable-in-band: true + dhcp6: true + eth1: + dhcp4: true + openvswitch: + other-config: + disable-in-band: false + bridges: + ovs0: + interfaces: [eth0, eth1] + openvswitch: {} +''') + self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0 +''' + OVS_BR_DEFAULT % {'iface': 'ovs0'}}, + 'eth0.service': OVS_PHYSICAL % {'iface': 'eth0', 'extra': '''\ +Requires=netplan-ovs-ovs0.service +After=netplan-ovs-ovs0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:iface-id=myhostname +ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/external-ids/iface-id=myhostname +ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:disable-in-band=true +ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/other-config/disable-in-band=true +'''}, + 'eth1.service': OVS_PHYSICAL % {'iface': 'eth1', 'extra': '''\ +Requires=netplan-ovs-ovs0.service +After=netplan-ovs-ovs0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false +ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'ovs0.network': ND_EMPTY % ('ovs0', 'ipv6'), + 'eth0.network': (ND_DHCP6 % 'eth0') + .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0'), + 'eth1.network': (ND_DHCP4 % 'eth1') + .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no\nBridge=ovs0')}) + + def test_interface_invalid_external_ids_other_config(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + openvswitch: + external-ids: + iface-id: myhostname + other-config: + disable-in-band: true''', expect_fail=True) + self.assertIn('eth0: Interface needs to be assigned to an OVS bridge/bond to carry external-ids/other-config', err) + + def test_global_external_ids_other_config(self): + self.generate('''network: + version: 2 + openvswitch: + external-ids: + iface-id: myhostname + other-config: + disable-in-band: true + ethernets: + eth0: + dhcp4: yes +''') + self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:iface-id=myhostname +ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/external-ids/iface-id=myhostname +ExecStart=/usr/bin/ovs-vsctl set open_vswitch . other-config:disable-in-band=true +ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/other-config/disable-in-band=true +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + + def test_global_set_protocols(self): + self.generate('''network: + version: 2 + openvswitch: + protocols: [OpenFlow10, OpenFlow11, OpenFlow12] + bridges: + ovs0: + openvswitch: {}''') + self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 +''' + OVS_BR_DEFAULT % {'iface': 'ovs0'} + '''\ +ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 protocols=OpenFlow10,OpenFlow11,OpenFlow12 +ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 external-ids:netplan/protocols=OpenFlow10,OpenFlow11,OpenFlow12 +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'ovs0.network': ND_EMPTY % ('ovs0', 'ipv6')}) + + def test_duplicate_map_entry(self): + err = self.generate('''network: + version: 2 + openvswitch: + external-ids: + iface-id: myhostname + iface-id: foobar + ethernets: + eth0: + dhcp4: yes +''', expect_fail=True) + self.assertIn("duplicate map entry 'iface-id'", err) + + def test_no_ovs_config(self): + self.generate('''network: + version: 2 + ethernets: + eth0: + dhcp4: yes +''') + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + + def test_bond_setup(self): + self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bonds: + bond0: + interfaces: [eth1, eth2] + openvswitch: + external-ids: + iface-id: myhostname + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] + openvswitch: {} +''') + self.assert_ovs({'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true +ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:iface-id=myhostname +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/external-ids/iface-id=myhostname +'''}, + 'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'bond0.network': ND_EMPTY % ('bond0', 'no')}) + + def test_bond_no_bridge(self): + err = self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bonds: + bond0: + interfaces: [eth1, eth2] + openvswitch: {} +''', expect_fail=True) + self.assertIn("Bond bond0 needs to be a slave of an OpenVSwitch bridge", err) + + def test_bond_not_enough_interfaces(self): + err = self.generate('''network: + version: 2 + ethernets: + eth1: {} + bonds: + bond0: + interfaces: [eth1] + openvswitch: {} + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] + openvswitch: {} +''', expect_fail=True) + self.assertIn("Bond bond0 needs to have at least 2 slave interfaces", err) + + def test_bond_lacp(self): + self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bonds: + bond0: + interfaces: [eth1, eth2] + openvswitch: + lacp: active + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] + openvswitch: {} +''') + self.assert_ovs({'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true +ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=active +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=active +'''}, + 'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'bond0.network': ND_EMPTY % ('bond0', 'no')}) + + def test_bond_lacp_invalid(self): + err = self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bonds: + bond0: + interfaces: [eth1, eth2] + openvswitch: + lacp: invalid + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] + openvswitch: {} +''', expect_fail=True) + self.assertIn("Value of 'lacp' needs to be 'active', 'passive' or 'off", err) + + def test_bond_lacp_wrong_type(self): + err = self.generate('''network: + version: 2 + ethernets: + eth1: + openvswitch: + lacp: passive +''', expect_fail=True) + self.assertIn("Key 'lacp' is only valid for interface type 'openvswitch bond'", err) + + def test_bond_mode_implicit_params(self): + self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bonds: + bond0: + interfaces: [eth1, eth2] + parameters: + mode: balance-tcp # Sets OVS backend implicitly + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] + openvswitch: {} +''') + self.assert_ovs({'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true +ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off +ExecStart=/usr/bin/ovs-vsctl set Port bond0 bond_mode=balance-tcp +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=balance-tcp +'''}, + 'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'bond0.network': ND_EMPTY % ('bond0', 'no')}) + + def test_bond_mode_explicit_params(self): + self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bonds: + bond0: + interfaces: [eth1, eth2] + parameters: + mode: active-backup + openvswitch: {} + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] + openvswitch: {} +''') + self.assert_ovs({'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true +ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off +ExecStart=/usr/bin/ovs-vsctl set Port bond0 bond_mode=active-backup +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=active-backup +'''}, + 'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n', + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'bond0.network': ND_EMPTY % ('bond0', 'no')}) + + def test_bond_mode_ovs_invalid(self): + err = self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bonds: + bond0: + interfaces: [eth1, eth2] + parameters: + mode: balance-rr + openvswitch: {} + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] + openvswitch: {} +''', expect_fail=True) + self.assertIn("bond0: bond mode 'balance-rr' not supported by openvswitch", err) + + def test_bridge_setup(self): + self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [eth1, eth2] + openvswitch: {} +''') + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': + ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 +''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24')}) + + def test_bridge_external_ids_other_config(self): + self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + external-ids: + iface-id: myhostname + other-config: + disable-in-band: true +''') + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 +''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:iface-id=myhostname +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/external-ids/iface-id=myhostname +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 other-config:disable-in-band=true +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/other-config/disable-in-band=true +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the bridge has been only configured for OVS + self.assert_networkd({'br0.network': ND_EMPTY % ('br0', 'ipv6')}) + + def test_bridge_non_default_parameters(self): + self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [eth1, eth2] + openvswitch: + fail-mode: secure + mcast-snooping: true + rstp: true +''') + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': + ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan=true +ExecStart=/usr/bin/ovs-vsctl set-fail-mode br0 secure +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/global/set-fail-mode=secure +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 mcast_snooping_enable=true +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/mcast_snooping_enable=true +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 rstp_enable=true +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/rstp_enable=true +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'eth1.network': '[Match]\nName=eth1\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24')}) + + def test_bridge_fail_mode_invalid(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + fail-mode: glorious +''', expect_fail=True) + self.assertIn("Value of 'fail-mode' needs to be 'standalone' or 'secure'", err) + + def test_fail_mode_non_bridge(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + openvswitch: + fail-mode: glorious +''', expect_fail=True) + self.assertIn("Key 'fail-mode' is only valid for interface type 'openvswitch bridge'", err) + + def test_rstp_non_bridge(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + openvswitch: + rstp: true +''', expect_fail=True) + self.assertIn("Key is only valid for interface type 'openvswitch bridge'", err) + + def test_bridge_set_protocols(self): + self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + protocols: [OpenFlow10, OpenFlow11, OpenFlow15] +''') + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': + ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 +''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 protocols=OpenFlow10,OpenFlow11,OpenFlow15 +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/protocols=OpenFlow10,OpenFlow11,OpenFlow15 +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'br0.network': ND_EMPTY % ('br0', 'ipv6')}) + + def test_bridge_set_protocols_invalid(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + protocols: [OpenFlow10, OpenFooBar13, OpenFlow15] +''', expect_fail=True) + self.assertIn("Unsupported OVS 'protocol' value: OpenFooBar13", err) + + def test_set_protocols_invalid_interface(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + openvswitch: + protocols: [OpenFlow10, OpenFlow15] +''', expect_fail=True) + self.assertIn("Key 'protocols' is only valid for interface type 'openvswitch bridge'", err) + + def test_bridge_controller(self): + self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + controller: + addresses: ["ptcp:", "ptcp:1337", "ptcp:1337:[fe80::1234%eth0]", "pssl:1337:[fe80::1]", "ssl:10.10.10.1",\ + tcp:127.0.0.1:1337, "tcp:[fe80::1234%eth0]", "tcp:[fe80::1]:1337", unix:/some/path, punix:other/path] + connection-mode: out-of-band + openvswitch: + ssl: + ca-cert: /another/path + certificate: /some/path + private-key: /key/path +''') + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': + ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 +''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ +ExecStart=/usr/bin/ovs-vsctl set-controller br0 ptcp: ptcp:1337 ptcp:1337:[fe80::1234%eth0] pssl:1337:[fe80::1] ssl:10.10.10.1 \ +tcp:127.0.0.1:1337 tcp:[fe80::1234%eth0] tcp:[fe80::1]:1337 unix:/some/path punix:other/path +ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/global/set-controller=ptcp:,ptcp:1337,\ +ptcp:1337:[fe80::1234%eth0],pssl:1337:[fe80::1],ssl:10.10.10.1,tcp:127.0.0.1:1337,tcp:[fe80::1234%eth0],tcp:[fe80::1]:1337,\ +unix:/some/path,punix:other/path +ExecStart=/usr/bin/ovs-vsctl set Controller br0 connection-mode=out-of-band +ExecStart=/usr/bin/ovs-vsctl set Controller br0 external-ids:netplan/connection-mode=out-of-band +'''}, + 'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path +ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'br0.network': ND_EMPTY % ('br0', 'ipv6')}) + + def test_bridge_controller_invalid_target(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + controller: + addresses: [ptcp] +''', expect_fail=True) + self.assertIn("Unsupported OVS controller target: ptcp", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_bridge_controller_invalid_target_ip(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + controller: + addresses: ["tcp:[fe80:1234%eth0]"] +''', expect_fail=True) + self.assertIn("Unsupported OVS controller target: tcp:[fe80:1234%eth0]", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_bridge_controller_invalid_target_port(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + controller: + addresses: [ptcp:65536] +''', expect_fail=True) + self.assertIn("Unsupported OVS controller target: ptcp:65536", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_bridge_controller_invalid_connection_mode(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + controller: + connection-mode: INVALID +''', expect_fail=True) + self.assertIn("Value of 'connection-mode' needs to be 'in-band' or 'out-of-band'", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_bridge_controller_connection_mode_invalid_interface_type(self): + err = self.generate('''network: + version: 2 + bonds: + mybond: + openvswitch: + controller: + connection-mode: in-band +''', expect_fail=True) + self.assertIn("Key 'controller.connection-mode' is only valid for interface type 'openvswitch bridge'", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_bridge_controller_addresses_invalid_interface_type(self): + err = self.generate('''network: + version: 2 + bonds: + mybond: + openvswitch: + controller: + addresses: [unix:/some/socket] +''', expect_fail=True) + self.assertIn("Key 'controller.addresses' is only valid for interface type 'openvswitch bridge'", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_global_ssl(self): + self.generate('''network: + version: 2 + openvswitch: + ssl: + ca-cert: /another/path + certificate: /some/path + private-key: /key/path +''') + self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path +ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({}) + + def test_missing_ssl(self): + err = self.generate('''network: + version: 2 + bridges: + br0: + openvswitch: + controller: + addresses: [ssl:10.10.10.1] + openvswitch: + ssl: {} +''', expect_fail=True) + self.assertIn("ERROR: openvswitch bridge controller target 'ssl:10.10.10.1' needs SSL configuration, but global \ +'openvswitch.ssl' settings are not set", err) + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({}) + + def test_global_ports(self): + err = self.generate('''network: + version: 2 + openvswitch: + ports: + - [patch0-1, patch1-0] +''', expect_fail=True) + self.assertIn('patch0-1: OpenVSwitch patch port needs to be assigned to a bridge/bond', err) + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({}) + + def test_few_ports(self): + err = self.generate('''network: + version: 2 + openvswitch: + ports: + - [patch0-1] +''', expect_fail=True) + self.assertIn("An openvswitch peer port sequence must have exactly two entries", err) + self.assertIn("- [patch0-1]", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_many_ports(self): + err = self.generate('''network: + version: 2 + openvswitch: + ports: + - [patch0-1, "patchx", patchy] +''', expect_fail=True) + self.assertIn("An openvswitch peer port sequence must have exactly two entries", err) + self.assertIn("- [patch0-1, \"patchx\", patchy]", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_ovs_invalid_port(self): + err = self.generate('''network: + version: 2 + openvswitch: + ports: + - [patchx, patchy] + - [patchx, patchz] +''', expect_fail=True) + self.assertIn("openvswitch port 'patchx' is already assigned to peer 'patchy'", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_ovs_invalid_peer(self): + err = self.generate('''network: + version: 2 + openvswitch: + ports: + - [patchx, patchy] + - [patchz, patchx] +''', expect_fail=True) + self.assertIn("openvswitch port 'patchx' is already assigned to peer 'patchy'", err) + self.assert_ovs({}) + self.assert_networkd({}) + + def test_bridge_auto_ovs_backend(self): + self.generate('''network: + version: 2 + ethernets: + eth1: {} + eth2: {} + bonds: + bond0: + interfaces: [eth1, eth2] + openvswitch: {} + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] +''') + self.assert_ovs({'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, + 'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true +ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'bond0.network': ND_EMPTY % ('bond0', 'no'), + 'eth1.network': + '''[Match] +Name=eth1 + +[Network] +LinkLocalAddressing=no +Bond=bond0 +''', + 'eth2.network': + '''[Match] +Name=eth2 + +[Network] +LinkLocalAddressing=no +Bond=bond0 +'''}) + + def test_bond_auto_ovs_backend(self): + self.generate('''network: + version: 2 + ethernets: + eth0: {} + bonds: + bond0: + interfaces: [eth0, patchy] + bridges: + br0: + addresses: [192.170.1.1/24] + interfaces: [bond0] + br1: + addresses: [2001:FFfe::1/64] + interfaces: [patchx] + openvswitch: + ports: + - [patchx, patchy] +''') + self.assert_ovs({'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, + 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patchx -- set Interface patchx type=patch options:peer=patchy +''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, + 'bond0.service': OVS_VIRTUAL % {'iface': 'bond0', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 patchy eth0 -- set Interface patchy type=patch options:peer=patchx +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true +ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=off +'''}, + 'patchx.service': OVS_VIRTUAL % {'iface': 'patchx', 'extra': + '''Requires=netplan-ovs-br1.service +After=netplan-ovs-br1.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set Port patchx external-ids:netplan=true +'''}, + 'patchy.service': OVS_VIRTUAL % {'iface': 'patchy', 'extra': + '''Requires=netplan-ovs-bond0.service +After=netplan-ovs-bond0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'br1.network': ND_WITHIP % ('br1', '2001:FFfe::1/64'), + 'bond0.network': ND_EMPTY % ('bond0', 'no'), + 'patchx.network': ND_EMPTY % ('patchx', 'no'), + 'patchy.network': ND_EMPTY % ('patchy', 'no'), + 'eth0.network': '[Match]\nName=eth0\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n'}) + + def test_patch_ports(self): + self.generate('''network: + version: 2 + openvswitch: + ports: + - [patch0-1, patch1-0] + bridges: + br0: + addresses: [192.168.1.1/24] + interfaces: [patch0-1] + br1: + addresses: [192.168.1.2/24] + interfaces: [patch1-0] +''') + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 patch0-1 -- set Interface patch0-1 type=patch options:peer=patch1-0 +''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, + 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patch1-0 -- set Interface patch1-0 type=patch options:peer=patch0-1 +''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, + 'patch0-1.service': OVS_VIRTUAL % {'iface': 'patch0-1', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set Port patch0-1 external-ids:netplan=true +'''}, + 'patch1-0.service': OVS_VIRTUAL % {'iface': 'patch1-0', 'extra': + '''Requires=netplan-ovs-br1.service +After=netplan-ovs-br1.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), + 'br1.network': ND_WITHIP % ('br1', '192.168.1.2/24'), + 'patch0-1.network': ND_EMPTY % ('patch0-1', 'no'), + 'patch1-0.network': ND_EMPTY % ('patch1-0', 'no')}) + + def test_fake_vlan_bridge_setup(self): + self.generate('''network: + version: 2 + bridges: + br0: + addresses: [192.168.1.1/24] + openvswitch: {} + vlans: + br0.100: + id: 100 + link: br0 + openvswitch: {} +''') + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 +''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, + 'br0.100.service': OVS_VIRTUAL % {'iface': 'br0.100', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 +ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), + 'br0.100.network': ND_EMPTY % ('br0.100', 'ipv6')}) + + def test_implicit_fake_vlan_bridge_setup(self): + # Test if, when a VLAN is added to an OVS bridge, netplan will + # implicitly assume the vlan should be done via OVS as well + self.generate('''network: + version: 2 + bridges: + br0: + addresses: [192.168.1.1/24] + openvswitch: {} + vlans: + br0.100: + id: 100 + link: br0 +''') + self.assert_ovs({'br0.service': OVS_BR_EMPTY % {'iface': 'br0'}, + 'br0.100.service': OVS_VIRTUAL % {'iface': 'br0.100', 'extra': + '''Requires=netplan-ovs-br0.service +After=netplan-ovs-br0.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 +ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true +'''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), + 'br0.100.network': ND_EMPTY % ('br0.100', 'ipv6')}) + + def test_invalid_device_type(self): + err = self.generate('''network: + version: 2 + ethernets: + eth0: + openvswitch: {} +''', expect_fail=True) + self.assertIn('eth0: This device type is not supported with the OpenVSwitch backend', err) + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({}) + + def test_bridge_non_ovs_bond(self): + self.generate('''network: + version: 2 + ethernets: + eth0: {} + eth1: {} + bonds: + non-ovs-bond: + interfaces: [eth0, eth1] + bridges: + ovs-br: + interfaces: [non-ovs-bond] + openvswitch: {} +''') + self.assert_ovs({'ovs-br.service': OVS_VIRTUAL % {'iface': 'ovs-br', 'extra': ''' +[Service] +Type=oneshot +ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs-br +ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs-br non-ovs-bond +''' + OVS_BR_DEFAULT % {'iface': 'ovs-br'}}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + # Confirm that the networkd config is still sane + self.assert_networkd({'non-ovs-bond.network': ND_EMPTY % ('non-ovs-bond', 'no') + 'Bridge=ovs-br\n', + 'eth1.network': (ND_EMPTY % ('eth1', 'no')).replace('ConfigureWithoutCarrier=yes', + 'Bond=non-ovs-bond'), + 'eth0.network': (ND_EMPTY % ('eth0', 'no')).replace('ConfigureWithoutCarrier=yes', + 'Bond=non-ovs-bond'), + 'ovs-br.network': ND_EMPTY % ('ovs-br', 'ipv6'), + 'non-ovs-bond.netdev': '[NetDev]\nName=non-ovs-bond\nKind=bond\n'}) diff --git a/tests/generator/test_passthrough.py b/tests/generator/test_passthrough.py new file mode 100644 index 0000000..817aaa0 --- /dev/null +++ b/tests/generator/test_passthrough.py @@ -0,0 +1,286 @@ +# +# Tests for passthrough config generated via netplan +# +# Copyright (C) 2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import TestBase + + +# No passthrough mode (yet) for systemd-networkd +class TestNetworkd(TestBase): + pass + + +class TestNetworkManager(TestBase): + + def test_passthrough_basic(self): + self.generate('''network: + version: 2 + ethernets: + NM-87749f1d-334f-40b2-98d4-55db58965f5f: + renderer: NetworkManager + match: {} + networkmanager: + uuid: 87749f1d-334f-40b2-98d4-55db58965f5f + name: some NM id + passthrough: + connection.uuid: 87749f1d-334f-40b2-98d4-55db58965f5f + connection.type: ethernet + connection.permissions: ""''') + + self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f': '''[connection] +id=some NM id +type=ethernet +uuid=87749f1d-334f-40b2-98d4-55db58965f5f +#Netplan: passthrough setting +permissions= + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_passthrough_wifi(self): + self.generate('''network: + version: 2 + wifis: + NM-87749f1d-334f-40b2-98d4-55db58965f5f: + renderer: NetworkManager + match: {} + access-points: + "SOME-SSID": + networkmanager: + uuid: 87749f1d-334f-40b2-98d4-55db58965f5f + name: myid with spaces + passthrough: + connection.permissions: "" + wifi.ssid: SOME-SSID + "OTHER-SSID": + hidden: true''') + + self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f-SOME-SSID': '''[connection] +id=myid with spaces +type=wifi +uuid=87749f1d-334f-40b2-98d4-55db58965f5f +#Netplan: passthrough setting +permissions= + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=SOME-SSID +mode=infrastructure +''', + 'NM-87749f1d-334f-40b2-98d4-55db58965f5f-OTHER-SSID': '''[connection] +id=netplan-NM-87749f1d-334f-40b2-98d4-55db58965f5f-OTHER-SSID +type=wifi + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=OTHER-SSID +mode=infrastructure +hidden=true +'''}) + + def test_passthrough_type_nm_devices(self): + self.generate('''network: + nm-devices: + NM-87749f1d-334f-40b2-98d4-55db58965f5f: + renderer: NetworkManager + match: {} + networkmanager: + passthrough: + connection.uuid: 87749f1d-334f-40b2-98d4-55db58965f5f + connection.type: dummy''') + + self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f': '''[connection] +id=netplan-NM-87749f1d-334f-40b2-98d4-55db58965f5f +#Netplan: passthrough setting +uuid=87749f1d-334f-40b2-98d4-55db58965f5f +#Netplan: passthrough setting +type=dummy + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_passthrough_dotted_group(self): + self.generate('''network: + nm-devices: + dotted-group-test: + renderer: NetworkManager + match: {} + networkmanager: + passthrough: + connection.type: "wireguard" + wireguard-peer.some-key.endpoint: 1.2.3.4''') + + self.assert_nm({'dotted-group-test': '''[connection] +id=netplan-dotted-group-test +#Netplan: passthrough setting +type=wireguard + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wireguard-peer.some-key] +#Netplan: passthrough setting +endpoint=1.2.3.4 +'''}) + + def test_passthrough_dotted_key(self): + self.generate('''network: + ethernets: + dotted-key-test: + renderer: NetworkManager + match: {} + networkmanager: + passthrough: + tc.qdisc.root: something + tc.qdisc.fff1: ":abc" + tc.filters.test: "test"''') + + self.assert_nm({'dotted-key-test': '''[connection] +id=netplan-dotted-key-test +type=ethernet + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[tc] +#Netplan: passthrough setting +qdisc.root=something +#Netplan: passthrough setting +qdisc.fff1=:abc +#Netplan: passthrough setting +filters.test=test +'''}) + + def test_passthrough_unsupported_setting(self): + self.generate('''network: + wifis: + test: + renderer: NetworkManager + match: {} + access-points: + "SOME-SSID": # implicit "mode: infrasturcutre" + networkmanager: + passthrough: + wifi.mode: "mesh"''') + + self.assert_nm({'test-SOME-SSID': '''[connection] +id=netplan-test-SOME-SSID +type=wifi + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=SOME-SSID +#Netplan: passthrough override +mode=mesh +'''}) + + def test_passthrough_empty_group(self): + self.generate('''network: + ethernets: + test: + renderer: NetworkManager + match: {} + networkmanager: + passthrough: + proxy._: ""''') + + self.assert_nm({'test': '''[connection] +id=netplan-test +type=ethernet + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[proxy] +'''}) + + def test_passthrough_interface_rename_existing_id(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + # This is the original netdef, generating "netplan-eth0.nmconnection" + eth0: + dhcp4: true + # This is the override netdef, modifying match.original_name (i.e. interface-name) + # it should still generate a "netplan-eth0.nmconnection" file (not netplan-eth33.nmconnection). + eth0: + renderer: NetworkManager + dhcp4: true + match: + name: "eth33" + networkmanager: + uuid: 626dd384-8b3d-3690-9511-192b2c79b3fd + name: "netplan-eth0" +''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +uuid=626dd384-8b3d-3690-9511-192b2c79b3fd +interface-name=eth33 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) diff --git a/tests/generator/test_routing.py b/tests/generator/test_routing.py new file mode 100644 index 0000000..9b302a9 --- /dev/null +++ b/tests/generator/test_routing.py @@ -0,0 +1,1333 @@ +# +# Routing / IP rule tests for netplan generator +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import TestBase + + +class TestNetworkd(TestBase): + + def test_route_invalid_family_to(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: abc/24 + via: 192.168.14.20''', expect_fail=True) + self.assertIn("Error in network definition: invalid IP family '-1'", err) + + def test_route_v4_single(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +'''}) + + def test_route_v4_single_mulit_parse(self): + self.generate('''network: + version: 2 + bridges: + br0: {interfaces: [engreen]} + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=no +Address=192.168.14.2/24 +Bridge=br0 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +''', + 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n', + 'br0.network': '''[Match]\nName=br0\n +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +'''}) + + def test_route_v4_multiple(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 8.8.0.0/16 + via: 192.168.1.1 + - to: 10.10.10.8 + via: 192.168.1.2 + metric: 5000 + - to: 11.11.11.0/24 + via: 192.168.1.3 + metric: 9999 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=8.8.0.0/16 +Gateway=192.168.1.1 + +[Route] +Destination=10.10.10.8 +Gateway=192.168.1.2 +Metric=5000 + +[Route] +Destination=11.11.11.0/24 +Gateway=192.168.1.3 +Metric=9999 +'''}) + + def test_route_v4_default(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.1.2/24"] + routes: + - to: default + via: 192.168.1.1 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.1.2/24 + +[Route] +Destination=0.0.0.0/0 +Gateway=192.168.1.1 +'''}) + + def test_route_v4_onlink(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + on-link: true + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +GatewayOnlink=true +Metric=100 +'''}) + + def test_route_v4_onlink_no(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + on-link: n + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +'''}) + + def test_route_v4_scope(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + scope: link + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Scope=link +Metric=100 +'''}) + + def test_route_v4_scope_redefine(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + scope: host + via: 192.168.14.20 + scope: link + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Scope=link +Metric=100 +'''}) + + def test_route_v4_type_blackhole(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + type: blackhole + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Type=blackhole +Metric=100 +'''}) + + def test_route_v4_type_redefine(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + type: prohibit + via: 192.168.14.20 + type: unicast + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +'''}) + + def test_route_v4_table(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + table: 201 + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +Metric=100 +Table=201 +'''}) + + def test_route_v4_from(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + from: 192.168.14.2 + metric: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +PreferredSource=192.168.14.2 +Metric=100 +'''}) + + def test_route_v4_mtu(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + mtu: 1500 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +MTUBytes=1500 +'''}) + + def test_route_v4_congestion_window(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + congestion-window: 16 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +InitialCongestionWindow=16 +'''}) + + def test_route_v4_advertised_receive_window(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + advertised-receive-window: 16 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +InitialAdvertisedReceiveWindow=16 +'''}) + + def test_route_v6_single(self): + self.generate('''network: + version: 2 + ethernets: + enblue: + addresses: ["192.168.1.3/24"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1''') + + self.assert_networkd({'enblue.network': '''[Match] +Name=enblue + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.1.3/24 + +[Route] +Destination=2001:dead:beef::2/64 +Gateway=2001:beef:beef::1 +'''}) + + def test_route_v6_type(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + type: prohibit''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=2001:dead:beef::2/64 +Gateway=2001:beef:beef::1 +Type=prohibit +'''}) + + def test_route_v6_scope_host(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + scope: host''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=2001:dead:beef::2/64 +Gateway=2001:beef:beef::1 +Scope=host +'''}) + + def test_route_v6_multiple(self): + self.generate('''network: + version: 2 + ethernets: + enblue: + addresses: ["192.168.1.3/24"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + - to: 2001:f00f:f00f::fe/64 + via: 2001:beef:feed::1 + metric: 1024''') + + self.assert_networkd({'enblue.network': '''[Match] +Name=enblue + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.1.3/24 + +[Route] +Destination=2001:dead:beef::2/64 +Gateway=2001:beef:beef::1 + +[Route] +Destination=2001:f00f:f00f::fe/64 +Gateway=2001:beef:feed::1 +Metric=1024 +'''}) + + def test_route_v6_default(self): + self.generate('''network: + version: 2 + ethernets: + enblue: + addresses: ["2001:dead:beef::2/64"] + routes: + - to: default + via: 2001:beef:beef::1''') + + self.assert_networkd({'enblue.network': '''[Match] +Name=enblue + +[Network] +LinkLocalAddressing=ipv6 +Address=2001:dead:beef::2/64 + +[Route] +Destination=::/0 +Gateway=2001:beef:beef::1 +'''}) + + def test_ip_rule_table(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + table: 100 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +Table=100 +'''}) + + def test_ip_rule_priority(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + priority: 99 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +Priority=99 +'''}) + + def test_ip_rule_fwmark(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - from: 10.10.10.0/24 + mark: 50 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +From=10.10.10.0/24 +FirewallMark=50 +'''}) + + def test_ip_rule_tos(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routing-policy: + - to: 10.10.10.0/24 + type-of-service: 250 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[RoutingPolicyRule] +To=10.10.10.0/24 +TypeOfService=250 +'''}) + + def test_use_routes(self): + """[networkd] Validate config generation when use-routes DHCP override is used""" + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: true + dhcp4-overrides: + use-routes: false + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +UseRoutes=false +'''}) + + def test_default_metric(self): + """[networkd] Validate config generation when metric DHCP override is used""" + self.generate('''network: + version: 2 + ethernets: + engreen: + dhcp4: true + dhcp6: true + dhcp4-overrides: + route-metric: 3333 + dhcp6-overrides: + route-metric: 3333 + enred: + dhcp4: true + dhcp6: true + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +DHCP=yes +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=3333 +UseMTU=true +''', + 'enred.network': '''[Match] +Name=enred + +[Network] +DHCP=yes +LinkLocalAddressing=ipv6 + +[DHCP] +RouteMetric=100 +UseMTU=true +'''}) + + +class TestNetworkManager(TestBase): + + def test_route_v4_single(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + metric: 100 + ''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.14.20,100 + +[ipv6] +method=ignore +'''}) + + def test_route_v4_multiple(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 8.8.0.0/16 + via: 192.168.1.1 + metric: 5000 + - to: 10.10.10.8 + via: 192.168.1.2 + - to: 11.11.11.0/24 + via: 192.168.1.3 + metric: 9999 + ''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=8.8.0.0/16,192.168.1.1,5000 +route2=10.10.10.8,192.168.1.2 +route3=11.11.11.0/24,192.168.1.3,9999 + +[ipv6] +method=ignore +'''}) + + def test_route_v4_default(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.1.2/24"] + routes: + - to: default + via: 192.168.1.1 + ''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.1.2/24 +route1=0.0.0.0/0,192.168.1.1 + +[ipv6] +method=ignore +'''}) + + def test_route_v6_single(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + enblue: + addresses: ["2001:f00f:f00f::2/64"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1''') + + self.assert_nm({'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=manual +address1=2001:f00f:f00f::2/64 +route1=2001:dead:beef::2/64,2001:beef:beef::1 +'''}) + + def test_route_v6_multiple(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + enblue: + addresses: ["2001:f00f:f00f::2/64"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + - to: 2001:dead:feed::2/64 + via: 2001:beef:beef::2 + metric: 1000''') + + self.assert_nm({'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=manual +address1=2001:f00f:f00f::2/64 +route1=2001:dead:beef::2/64,2001:beef:beef::1 +route2=2001:dead:feed::2/64,2001:beef:beef::2,1000 +'''}) + + def test_route_v6_default(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + enblue: + addresses: ["2001:dead:beef::2/64"] + routes: + - to: default + via: 2001:beef:beef::1''') + + self.assert_nm({'enblue': '''[connection] +id=netplan-enblue +type=ethernet +interface-name=enblue + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=manual +address1=2001:dead:beef::2/64 +route1=::/0,2001:beef:beef::1 +'''}) + + def test_routes_mixed(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + addresses: ["192.168.14.2/24", "2001:f00f::2/128"] + routes: + - to: 2001:dead:beef::2/64 + via: 2001:beef:beef::1 + metric: 997 + - to: 8.8.0.0/16 + via: 192.168.1.1 + metric: 5000 + - to: 10.10.10.8 + via: 192.168.1.2 + - to: 11.11.11.0/24 + via: 192.168.1.3 + metric: 9999 + - to: 2001:f00f:f00f::fe/64 + via: 2001:beef:feed::1 + ''') + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=8.8.0.0/16,192.168.1.1,5000 +route2=10.10.10.8,192.168.1.2 +route3=11.11.11.0/24,192.168.1.3,9999 + +[ipv6] +method=manual +address1=2001:f00f::2/128 +route1=2001:dead:beef::2/64,2001:beef:beef::1,997 +route2=2001:f00f:f00f::fe/64,2001:beef:feed::1 +'''}) + + def test_route_from(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + from: 192.168.14.2 + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.14.20 +route1_options=src=192.168.14.2 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_route_onlink(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + on-link: true + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.1.20 +route1_options=onlink=true + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_route_table(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + table: 31337 + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.1.20 +route1_options=table=31337 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_route_mtu(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + mtu: 1500 + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.1.20 +route1_options=mtu=1500 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_route_congestion_window(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + congestion-window: 16 + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.1.20 +route1_options=initcwnd=16 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_route_advertised_receive_window(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + advertised-receive-window: 16 + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.1.20 +route1_options=initrwnd=16 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_route_options(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + table: 31337 + from: 192.168.14.2 + on-link: true + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.1.20 +route1_options=onlink=true,table=31337,src=192.168.14.2 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_route_reject_scope(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + scope: host + ''', expect_fail=True) + self.assertIn('ERROR: engreen: NetworkManager does not support setting a scope for routes', out) + + self.assert_nm({}) + self.assert_networkd({}) + + def test_route_reject_type(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + type: blackhole + ''', expect_fail=True) + self.assertIn('NetworkManager only supports unicast routes', err) + + self.assert_nm({}) + self.assert_networkd({}) + + def test_use_routes_v4(self): + """[NetworkManager] Validate config when use-routes DHCP4 override is used""" + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true + dhcp4-overrides: + use-routes: false + ''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto +ignore-auto-routes=true +never-default=true + +[ipv6] +method=ignore +'''}) + + def test_use_routes_v6(self): + """[NetworkManager] Validate config when use-routes DHCP6 override is used""" + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true + dhcp6: true + dhcp6-overrides: + use-routes: false + ''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=auto +ignore-auto-routes=true +never-default=true +'''}) + + def test_default_metric_v4(self): + """[NetworkManager] Validate config when setting a default metric for DHCPv4""" + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true + dhcp6: true + dhcp4-overrides: + route-metric: 4000 + ''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto +route-metric=4000 + +[ipv6] +method=auto +'''}) + + def test_default_metric_v6(self): + """[NetworkManager] Validate config when setting a default metric for DHCPv6""" + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + engreen: + dhcp4: true + dhcp6: true + dhcp6-overrides: + route-metric: 5050 + ''') + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=auto +route-metric=5050 +'''}) diff --git a/tests/generator/test_tunnels.py b/tests/generator/test_tunnels.py new file mode 100644 index 0000000..534c2ba --- /dev/null +++ b/tests/generator/test_tunnels.py @@ -0,0 +1,1409 @@ +# +# Tests for tunnel devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import TestBase, ND_WITHIPGW, ND_EMPTY, NM_WG, ND_WG + + +def prepare_config_for_mode(renderer, mode, key=None, ttl=None): + config = """network: + version: 2 + renderer: {} +""".format(renderer) + + if mode == "ip6gre" \ + or mode == "ip6ip6" \ + or mode == "vti6" \ + or mode == "ipip6" \ + or mode == "ip6gretap": + local_ip = "fe80::dead:beef" + remote_ip = "2001:fe:ad:de:ad:be:ef:1" + else: + local_ip = "10.10.10.10" + remote_ip = "20.20.20.20" + + append_ttl = '\n ttl: {}'.format(ttl) if ttl else '' + config += """ + tunnels: + tun0: + mode: {} + local: {} + remote: {}{} + addresses: [ 15.15.15.15/24 ] + gateway4: 20.20.20.21 +""".format(mode, local_ip, remote_ip, append_ttl) + + # Handle key/keys as str or dict as required by the test + if type(key) is str: + config += """ + key: {} +""".format(key) + elif type(key) is dict: + config += """ + keys: + input: {} + output: {} +""".format(key['input'], key['output']) + + return config + + +def prepare_wg_config(listen=None, privkey=None, fwmark=None, peers=[], renderer="networkd"): + config = '''network: + version: 2 + renderer: %s + tunnels: + wg0: + mode: wireguard + addresses: [15.15.15.15/24, 2001:de:ad:be:ef:ca:fe:1/128] + gateway4: 20.20.20.21 +''' % renderer + if privkey is not None: + config += ' key: {}\n'.format(privkey) + if fwmark is not None: + config += ' mark: {}\n'.format(fwmark) + if listen is not None: + config += ' port: {}\n'.format(listen) + if len(peers) > 0: + config += ' peers:\n' + for peer in peers: + public_key = peer.get('public-key') + peer.pop('public-key', None) + shared_key = peer.get('shared-key') + peer.pop('shared-key', None) + pfx = ' - ' + for k, v in peer.items(): + config += '{}{}: {}\n'.format(pfx, k, v) + pfx = ' ' + if public_key or shared_key: + config += '{}keys:\n'.format(pfx) + if public_key: + config += ' public: {}\n'.format(public_key) + if shared_key: + config += ' shared: {}\n'.format(shared_key) + return config + + +class _CommonParserErrors(): + + def test_fail_invalid_private_key(self): + """[wireguard] Show an error for an invalid private key""" + config = prepare_wg_config(listen=12345, privkey='invalid.key', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: wg0: invalid wireguard private key", out) + + def test_fail_invalid_public_key(self): + """[wireguard] Show an error for an invalid private key""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': '/invalid.key', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: wg0: invalid wireguard public key", out) + + def test_fail_invalid_shared_key(self): + """[wireguard] Show an error for an invalid pre shared key""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'shared-key': 'invalid.key', + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: wg0: invalid wireguard shared key", out) + + def test_fail_keepalive_2big(self): + """[wireguard] Show an error if keepalive is too big""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 100500, + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: wg0: keepalive must be 0-65535 inclusive.", out) + + def test_fail_keepalive_bogus(self): + """[wireguard] Show an error if keepalive is not an int""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 'bogus', + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid unsigned int value 'bogus'", out) + + def test_fail_allowed_ips_prefix4(self): + """[wireguard] Show an error if ipv4 prefix is too big""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/200, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid prefix length in address", out) + + def test_fail_allowed_ips_prefix6(self): + """[wireguard] Show an error if ipv6 prefix too big""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/224"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid prefix length in address", out) + + def test_fail_allowed_ips_noprefix4(self): + """[wireguard] Show an error if ipv4 prefix is missing""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: address \'0.0.0.0\' is missing /prefixlength", out) + + def test_fail_allowed_ips_noprefix6(self): + """[wireguard] Show an error if ipv6 prefix is missing""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: address '2001:fe:ad:de:ad:be:ef:1' is missing /prefixlength", out) + + def test_fail_allowed_ips_bogus(self): + """[wireguard] Show an error if the address is completely bogus""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[302.302.302.302/24, "2001:fe:ad:de:ad:be:ef:1"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: malformed address \'302.302.302.302/24\', \ +must be X.X.X.X/NN or X:X:X:X:X:X:X:X/NN", out) + + def test_fail_remote_no_port4(self): + """[wireguard] Show an error if ipv4 remote endpoint lacks a port""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: endpoint '1.2.3.4' is missing :port", out) + + def test_fail_remote_no_port6(self): + """[wireguard] Show an error if ipv6 remote endpoint lacks a port""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': "2001:fe:ad:de:ad:be:ef:1"}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid endpoint address or hostname", out) + + def test_fail_remote_no_port_hn(self): + """[wireguard] Show an error if fqdn remote endpoint lacks a port""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': 'fq.dn'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: endpoint 'fq.dn' is missing :port", out) + + def test_fail_remote_big_port4(self): + """[wireguard] Show an error if ipv4 remote endpoint port is too big""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:100500'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid port in endpoint '1.2.3.4:100500", out) + + def test_fail_ipv6_remote_noport(self): + """[wireguard] Show an error for v6 remote endpoint without port""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'endpoint': '"[2001:fe:ad:de:ad:be:ef:11]"'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("endpoint \'[2001:fe:ad:de:ad:be:ef:11]\' is missing :port", out) + + def test_fail_ipv6_remote_nobrace(self): + """[wireguard] Show an error for v6 remote endpoint without closing brace""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'endpoint': '"[2001:fe:ad:de:ad:be:ef:11"'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("invalid address in endpoint '[2001:fe:ad:de:ad:be:ef:11'", out) + + def test_fail_ipv6_remote_malformed(self): + """[wireguard] Show an error for malformed-v6 remote endpoint""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'endpoint': '"[2001:fe:badfilinad:be:ef]:11"'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("invalid endpoint address or hostname '[2001:fe:badfilinad:be:ef]:11", out) + + def test_fail_short_remote(self): + """[wireguard] Show an error for too-short remote endpoint""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'endpoint': 'ab'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid endpoint address or hostname 'ab'", out) + + def test_fail_bogus_peer_key(self): + """[wireguard] Show an error for a bogus key in a peer""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'bogus': 'true', + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: unknown key 'bogus'", out) + + def test_fail_missing_private_key(self): + """[wireguard] Show an error for a missing private key""" + config = prepare_wg_config(listen=12345, + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: wg0: missing 'key' property (private key) for wireguard", out) + + def test_fail_no_peers(self): + """[wireguard] Show an error for missing peers""" + config = prepare_wg_config(listen=12345, privkey="4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=", renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: wg0: at least one peer is required.", out) + + def test_fail_no_public_key(self): + """[wireguard] Show an error for missing public_key""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: wg0: keys.public is required.", out) + + def test_fail_no_allowed_ips(self): + """[wireguard] Show an error for a missing allowed_ips""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'keepalive': 14, + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: wg0: 'to' is required to define the allowed IPs.", out) + + +class _CommonTests(): + + def test_simple(self): + """[wireguard] Validate generation of simple wireguard config""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + fwmark=42, + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'shared-key': '7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8=', + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + self.generate(config) + if self.backend == 'networkd': + self.assert_networkd({'wg0.netdev': ND_WG % ('=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', '''FwMark=42 + +[WireGuardPeer] +PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= +AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 +PersistentKeepalive=23 +Endpoint=1.2.3.4:5 +PresharedKey=7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8='''), + 'wg0.network': ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', + '20.20.20.21')}) + elif self.backend == 'NetworkManager': + self.assert_nm({'wg0.nmconnection': NM_WG % ('4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', '''fwmark=42 + +[wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] +persistent-keepalive=23 +endpoint=1.2.3.4:5 +preshared-key=7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= +preshared-key-flags=0 +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) + + def test_simple_multi_pass(self): + """[wireguard] Validate generation of a wireguard config, which is parsed multiple times""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + config = config.replace('tunnels:', 'bridges: {br0: {interfaces: [wg0]}}\n tunnels:') + self.generate(config) + if self.backend == 'networkd': + self.assert_networkd({'wg0.netdev': ND_WG % ('=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' +[WireGuardPeer] +PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= +AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 +PersistentKeepalive=23 +Endpoint=1.2.3.4:5'''), + 'wg0.network': (ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', + '20.20.20.21') + 'Bridge=br0\n') + .replace('LinkLocalAddressing=ipv6', 'LinkLocalAddressing=no'), + 'br0.network': ND_EMPTY % ('br0', 'ipv6'), + 'br0.netdev': '''[NetDev]\nName=br0\nKind=bridge\n'''}) + elif self.backend == 'NetworkManager': + self.assert_nm({'wg0.nmconnection': '''[connection] +id=netplan-wg0 +type=wireguard +interface-name=wg0 +slave-type=bridge +master=br0 + +[wireguard] +private-key=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8= +listen-port=12345 + +[wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] +persistent-keepalive=23 +endpoint=1.2.3.4:5 +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24; + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=manual +address1=2001:de:ad:be:ef:ca:fe:1/128 +''', + 'br0.nmconnection': '''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_2peers(self): + """[wireguard] Validate generation of wireguard config with two peers""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'endpoint': '1.2.3.4:5'}, { + 'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + self.generate(config) + if self.backend == 'networkd': + self.assert_networkd({'wg0.netdev': ND_WG % ('=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' +[WireGuardPeer] +PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= +AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 +PersistentKeepalive=23 +Endpoint=1.2.3.4:5 + +[WireGuardPeer] +PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5= +AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 +PersistentKeepalive=23 +Endpoint=1.2.3.4:5'''), + 'wg0.network': ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', + '20.20.20.21')}) + elif self.backend == 'NetworkManager': + self.assert_nm({'wg0.nmconnection': NM_WG % ('4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' +[wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] +persistent-keepalive=23 +endpoint=1.2.3.4:5 +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24; + +[wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5=] +persistent-keepalive=23 +endpoint=1.2.3.4:5 +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) + + def test_privatekeyfile(self): + """[wireguard] Validate generation of another simple wireguard config""" + config = prepare_wg_config(listen=12345, privkey='/tmp/test_private_key', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'shared-key': '/tmp/test_preshared_key', + 'endpoint': '1.2.3.4:5'}], renderer=self.backend) + if self.backend == 'networkd': + self.generate(config) + self.assert_networkd({'wg0.netdev': ND_WG % ('File=/tmp/test_private_key', '12345', ''' +[WireGuardPeer] +PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= +AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 +PersistentKeepalive=23 +Endpoint=1.2.3.4:5 +PresharedKeyFile=/tmp/test_preshared_key'''), + 'wg0.network': ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', + '20.20.20.21')}) + elif self.backend == 'NetworkManager': + err = self.generate(config, expect_fail=True) + self.assertIn('wg0: private key needs to be base64 encoded when using the NM backend', err) + + def test_ipv6_remote(self): + """[wireguard] Validate generation of wireguard config with v6 remote endpoint""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 23, + 'endpoint': '"[2001:fe:ad:de:ad:be:ef:11]:5"'}], renderer=self.backend) + self.generate(config) + if self.backend == 'networkd': + self.assert_networkd({'wg0.netdev': ND_WG % ('=4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' +[WireGuardPeer] +PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= +AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 +PersistentKeepalive=23 +Endpoint=[2001:fe:ad:de:ad:be:ef:11]:5'''), + 'wg0.network': ND_WITHIPGW % ('wg0', '15.15.15.15/24', '2001:de:ad:be:ef:ca:fe:1/128', + '20.20.20.21')}) + elif self.backend == 'NetworkManager': + self.assert_nm({'wg0.nmconnection': NM_WG % ('4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', '12345', ''' +[wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] +persistent-keepalive=23 +endpoint=[2001:fe:ad:de:ad:be:ef:11]:5 +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) + + +# Execute the _CommonParserErrors only for one backend, to spare some test cycles +class TestNetworkd(TestBase, _CommonTests, _CommonParserErrors): + backend = 'networkd' + + def test_sit(self): + """[networkd] Validate generation of SIT tunnels""" + config = prepare_config_for_mode('networkd', 'sit') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=sit + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_sit_he(self): + """[networkd] Validate generation of SIT tunnels (HE example)""" + # Test specifically a config like one that would enable Hurricane + # Electric IPv6 tunnels. + config = '''network: + version: 2 + renderer: networkd + ethernets: + eth0: + addresses: + - 1.1.1.1/24 + - "2001:cafe:face::1/64" # provided by HE as routed /64 + gateway4: 1.1.1.254 + 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" +''' + self.generate(config) + self.assert_networkd({'eth0.network': '''[Match] +Name=eth0 + +[Network] +LinkLocalAddressing=ipv6 +Address=1.1.1.1/24 +Address=2001:cafe:face::1/64 +Gateway=1.1.1.254 +''', + 'he-ipv6.netdev': '''[NetDev] +Name=he-ipv6 +Kind=sit + +[Tunnel] +Independent=true +Local=1.1.1.1 +Remote=2.2.2.2 +''', + 'he-ipv6.network': '''[Match] +Name=he-ipv6 + +[Network] +LinkLocalAddressing=ipv6 +Address=2001:dead:beef::2/64 +Gateway=2001:dead:beef::1 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti(self): + """[networkd] Validate generation of VTI tunnels""" + config = prepare_config_for_mode('networkd', 'vti') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti_with_key_str(self): + """[networkd] Validate generation of VTI tunnels with input/output keys""" + config = prepare_config_for_mode('networkd', 'vti', key='1.1.1.1') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +InputKey=1.1.1.1 +OutputKey=1.1.1.1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti_with_key_dict(self): + """[networkd] Validate generation of VTI tunnels with key dict""" + config = prepare_config_for_mode('networkd', 'vti', key={'input': 1234, 'output': 5678}) + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +InputKey=1234 +OutputKey=5678 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti_invalid_key(self): + """[networkd] Validate VTI tunnel generation key handling""" + config = prepare_config_for_mode('networkd', 'vti', key={'input': 42, 'output': 'invalid'}) + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid tunnel key 'invalid'", out) + + def test_vti6(self): + """[networkd] Validate generation of VTI6 tunnels""" + config = prepare_config_for_mode('networkd', 'vti6') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti6 + +[Tunnel] +Independent=true +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti6_with_key(self): + """[networkd] Validate generation of VTI6 tunnels with input/output keys""" + config = prepare_config_for_mode('networkd', 'vti6', key='1.1.1.1') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=vti6 + +[Tunnel] +Independent=true +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +InputKey=1.1.1.1 +OutputKey=1.1.1.1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_vti6_invalid_key(self): + """[networkd] Validate VTI6 tunnel generation key handling""" + config = prepare_config_for_mode('networkd', 'vti6', key='invalid') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid tunnel key 'invalid'", out) + + def test_ipip6(self): + """[networkd] Validate generation of IPIP6 tunnels""" + config = prepare_config_for_mode('networkd', 'ipip6') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=ip6tnl + +[Tunnel] +Independent=true +Mode=ipip6 +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_ipip(self): + """[networkd] Validate generation of IPIP tunnels""" + config = prepare_config_for_mode('networkd', 'ipip', ttl=64) + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=ipip + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +TTL=64 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_isatap(self): + """[networkd] Warning for ISATAP tunnel generation not supported""" + config = prepare_config_for_mode('networkd', 'isatap') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: ISATAP tunnel mode is not supported", out) + + def test_gre(self): + """[networkd] Validate generation of GRE tunnels""" + config = prepare_config_for_mode('networkd', 'gre') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=gre + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_ip6gre(self): + """[networkd] Validate generation of IP6GRE tunnels""" + config = prepare_config_for_mode('networkd', 'ip6gre') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=ip6gre + +[Tunnel] +Independent=true +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_gretap(self): + """[networkd] Validate generation of GRETAP tunnels""" + config = prepare_config_for_mode('networkd', 'gretap') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=gretap + +[Tunnel] +Independent=true +Local=10.10.10.10 +Remote=20.20.20.20 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + def test_ip6gretap(self): + """[networkd] Validate generation of IP6GRETAP tunnels""" + config = prepare_config_for_mode('networkd', 'ip6gretap') + self.generate(config) + self.assert_networkd({'tun0.netdev': '''[NetDev] +Name=tun0 +Kind=ip6gretap + +[Tunnel] +Independent=true +Local=fe80::dead:beef +Remote=2001:fe:ad:de:ad:be:ef:1 +''', + 'tun0.network': '''[Match] +Name=tun0 + +[Network] +LinkLocalAddressing=ipv6 +Address=15.15.15.15/24 +Gateway=20.20.20.21 +ConfigureWithoutCarrier=yes +'''}) + + +class TestNetworkManager(TestBase, _CommonTests): + backend = 'NetworkManager' + + def test_fail_invalid_private_key_file(self): + """[wireguard] Show an error for an invalid private key-file""" + config = prepare_wg_config(listen=12345, privkey='/invalid.key', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + + out = self.generate(config, expect_fail=True) + self.assertIn("wg0: private key needs to be base64 encoded when using the NM backend", out) + + def test_fail_invalid_shared_key_file(self): + """[wireguard] Show an error for an invalid pre shared key-file""" + config = prepare_wg_config(listen=12345, privkey='4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=', + peers=[{'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', + 'keepalive': 14, + 'shared-key': '/invalid.key', + 'endpoint': '1.2.3.4:1005'}], renderer=self.backend) + + out = self.generate(config, expect_fail=True) + self.assertIn("wg0: shared key needs to be base64 encoded when using the NM backend", out) + + def test_isatap(self): + """[NetworkManager] Validate ISATAP tunnel generation""" + config = prepare_config_for_mode('NetworkManager', 'isatap') + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=4 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_sit(self): + """[NetworkManager] Validate generation of SIT tunnels""" + config = prepare_config_for_mode('NetworkManager', 'sit') + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=3 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_sit_he(self): + """[NetworkManager] Validate generation of SIT tunnels (HE example)""" + # Test specifically a config like one that would enable Hurricane + # Electric IPv6 tunnels. + config = '''network: + version: 2 + renderer: NetworkManager + ethernets: + eth0: + addresses: + - 1.1.1.1/24 + - "2001:cafe:face::1/64" # provided by HE as routed /64 + gateway4: 1.1.1.254 + 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" +''' + self.generate(config) + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +interface-name=eth0 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=1.1.1.1/24 +gateway=1.1.1.254 + +[ipv6] +method=manual +address1=2001:cafe:face::1/64 +''', + 'he-ipv6': '''[connection] +id=netplan-he-ipv6 +type=ip-tunnel +interface-name=he-ipv6 + +[ip-tunnel] +mode=3 +local=1.1.1.1 +remote=2.2.2.2 + +[ipv4] +method=disabled + +[ipv6] +method=manual +address1=2001:dead:beef::2/64 +gateway=2001:dead:beef::1 +'''}) + + def test_vti(self): + """[NetworkManager] Validate generation of VTI tunnels""" + config = prepare_config_for_mode('NetworkManager', 'vti') + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=5 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_vti6(self): + """[NetworkManager] Validate generation of VTI6 tunnels""" + config = prepare_config_for_mode('NetworkManager', 'vti6') + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=9 +local=fe80::dead:beef +remote=2001:fe:ad:de:ad:be:ef:1 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_ip6ip6(self): + """[NetworkManager] Validate generation of IP6IP6 tunnels""" + config = prepare_config_for_mode('NetworkManager', 'ip6ip6') + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=6 +local=fe80::dead:beef +remote=2001:fe:ad:de:ad:be:ef:1 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_ipip(self): + """[NetworkManager] Validate generation of IPIP tunnels""" + config = prepare_config_for_mode('NetworkManager', 'ipip', ttl=64) + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=1 +local=10.10.10.10 +remote=20.20.20.20 +ttl=64 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_gre(self): + """[NetworkManager] Validate generation of GRE tunnels""" + config = prepare_config_for_mode('NetworkManager', 'gre') + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=2 +local=10.10.10.10 +remote=20.20.20.20 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_gre_with_keys(self): + """[NetworkManager] Validate generation of GRE tunnels with keys""" + config = prepare_config_for_mode('NetworkManager', 'gre', key={'input': 1111, 'output': 5555}) + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=2 +local=10.10.10.10 +remote=20.20.20.20 +input-key=1111 +output-key=5555 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_ip6gre(self): + """[NetworkManager] Validate generation of IP6GRE tunnels""" + config = prepare_config_for_mode('NetworkManager', 'ip6gre') + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=8 +local=fe80::dead:beef +remote=2001:fe:ad:de:ad:be:ef:1 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + def test_ip6gre_with_key(self): + """[NetworkManager] Validate generation of IP6GRE tunnels with key""" + config = prepare_config_for_mode('NetworkManager', 'ip6gre', key='9999') + self.generate(config) + self.assert_nm({'tun0': '''[connection] +id=netplan-tun0 +type=ip-tunnel +interface-name=tun0 + +[ip-tunnel] +mode=8 +local=fe80::dead:beef +remote=2001:fe:ad:de:ad:be:ef:1 +input-key=9999 +output-key=9999 + +[ipv4] +method=manual +address1=15.15.15.15/24 +gateway=20.20.20.21 + +[ipv6] +method=ignore +'''}) + + +class TestConfigErrors(TestBase): + + def test_missing_mode(self): + """Fail if tunnel mode is missing""" + config = '''network: + version: 2 + tunnels: + tun0: + remote: 20.20.20.20 + local: 10.10.10.10 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: missing 'mode' property for tunnel", out) + + def test_invalid_mode(self): + """Ensure an invalid tunnel mode shows an error message""" + config = prepare_config_for_mode('networkd', 'invalid') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: tunnel mode 'invalid' is not supported", out) + + def test_invalid_mode_for_nm(self): + """Show an error if a mode is selected that can't be handled by the renderer""" + config = prepare_config_for_mode('NetworkManager', 'gretap') + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: GRETAP tunnel mode is not supported by NetworkManager", out) + + def test_malformed_tunnel_ip(self): + """Fail if local/remote IP for tunnel are malformed""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + remote: 20.20.20.20 + local: 10.10.1invalid +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: malformed address '10.10.1invalid', must be X.X.X.X or X:X:X:X:X:X:X:X", out) + + def test_cidr_tunnel_ip(self): + """Fail if local/remote IP for tunnel include /prefix""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + remote: 20.20.20.20 + local: 10.10.10.10/21 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: address '10.10.10.10/21' should not include /prefixlength", out) + + def test_missing_local_ip(self): + """Fail if local IP is missing""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + remote: 20.20.20.20 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: missing 'local' property for tunnel", out) + + def test_missing_remote_ip(self): + """Fail if remote IP is missing""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + local: 20.20.20.20 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: missing 'remote' property for tunnel", out) + + def test_invalid_ttl(self): + """Fail if TTL not in range [1...255]""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: ipip + local: 20.20.20.20 + remote: 10.10.10.10 + ttl: 300 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'ttl' property for tunnel must be in range [1...255]", out) + + def test_wrong_local_ip_for_mode_v4(self): + """Show an error when an IPv6 local addr is used for an IPv4 tunnel mode""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + local: fe80::2 + remote: 20.20.20.20 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'local' must be a valid IPv4 address for this tunnel type", out) + + def test_wrong_remote_ip_for_mode_v4(self): + """Show an error when an IPv6 remote addr is used for an IPv4 tunnel mode""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: gre + local: 10.10.10.10 + remote: 2006::1 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'remote' must be a valid IPv4 address for this tunnel type", out) + + def test_wrong_local_ip_for_mode_v6(self): + """Show an error when an IPv4 local addr is used for an IPv6 tunnel mode""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: ip6gre + local: 10.10.10.10 + remote: 2001::3 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'local' must be a valid IPv6 address for this tunnel type", out) + + def test_wrong_remote_ip_for_mode_v6(self): + """Show an error when an IPv4 remote addr is used for an IPv6 tunnel mode""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: ip6gre + local: 2001::face + remote: 20.20.20.20 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'remote' must be a valid IPv6 address for this tunnel type", out) + + def test_malformed_keys(self): + """Show an error if tunnel keys stanza is malformed""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + - input: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: invalid type for 'key[s]': must be a scalar or mapping", out) + + def test_networkd_invalid_input_key_use(self): + """[networkd] Show an error if input-key is used for a mode that does not support it""" + config = '''network: + version: 2 + renderer: networkd + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + input: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'input-key' is not required for this tunnel type", out) + + def test_networkd_invalid_output_key_use(self): + """[networkd] Show an error if output-key is used for a mode that does not support it""" + config = '''network: + version: 2 + renderer: networkd + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + output: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'output-key' is not required for this tunnel type", out) + + def test_nm_invalid_input_key_use(self): + """[NetworkManager] Show an error if input-key is used for a mode that does not support it""" + config = '''network: + version: 2 + renderer: NetworkManager + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + input: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'input-key' is not required for this tunnel type", out) + + def test_nm_invalid_output_key_use(self): + """[NetworkManager] Show an error if output-key is used for a mode that does not support it""" + config = '''network: + version: 2 + renderer: NetworkManager + tunnels: + tun0: + mode: ipip + local: 10.10.10.10 + remote: 20.20.20.20 + keys: + output: 1234 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'output-key' is not required for this tunnel type", out) diff --git a/tests/generator/test_vlans.py b/tests/generator/test_vlans.py new file mode 100644 index 0000000..63827fd --- /dev/null +++ b/tests/generator/test_vlans.py @@ -0,0 +1,306 @@ +# +# Tests for VLAN devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import re +import unittest + +from .base import TestBase, ND_VLAN, ND_EMPTY, ND_WITHIP, ND_DHCP6_WOCARRIER + + +class TestNetworkd(TestBase): + + @unittest.skipIf("CODECOV_TOKEN" in os.environ, "Skipping on codecov.io: GLib changed hashtable elements order") + def test_vlan(self): # pragma: nocover + self.generate('''network: + version: 2 + ethernets: + en1: {} + vlans: + enblue: + id: 1 + link: en1 + addresses: [1.2.3.4/24] + enred: + id: 3 + link: en1 + macaddress: aa:bb:cc:dd:ee:11 + engreen: {id: 2, link: en1, dhcp6: true}''') + + self.assert_networkd({'en1.network': '''[Match] +Name=en1 + +[Network] +LinkLocalAddressing=ipv6 +VLAN=enblue +VLAN=enred +VLAN=engreen +''', + 'enblue.netdev': ND_VLAN % ('enblue', 1), + 'engreen.netdev': ND_VLAN % ('engreen', 2), + 'enred.netdev': '''[NetDev] +Name=enred +MACAddress=aa:bb:cc:dd:ee:11 +Kind=vlan + +[VLAN] +Id=3 +''', + 'enblue.network': ND_WITHIP % ('enblue', '1.2.3.4/24'), + 'enred.network': (ND_EMPTY % ('enred', 'ipv6')) + .replace('[Network]', '[Link]\nMACAddress=aa:bb:cc:dd:ee:11\n\n[Network]'), + 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) + + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:en1,interface-name:enblue,interface-name:enred,interface-name:engreen,''') + self.assert_nm_udev(None) + + def test_vlan_sriov(self): + # we need to make sure renderer: sriov vlans are not saved as part of + # the NM/networkd config + self.generate('''network: + version: 2 + ethernets: + en1: {} + vlans: + enblue: + id: 1 + link: en1 + renderer: sriov + engreen: {id: 2, link: en1, dhcp6: true}''') + + self.assert_networkd({'en1.network': '''[Match] +Name=en1 + +[Network] +LinkLocalAddressing=ipv6 +VLAN=engreen +''', + 'engreen.netdev': ND_VLAN % ('engreen', 2), + 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) + + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:en1,interface-name:enblue,interface-name:engreen,''') + self.assert_nm_udev(None) + + # see LP: #1888726 + def test_vlan_parent_match(self): + self.generate('''network: + version: 2 + renderer: networkd + ethernets: + lan: + match: {macaddress: "11:22:33:44:55:66"} + set-name: lan + mtu: 9000 + vlans: + vlan20: {id: 20, link: lan}''') + + self.assert_networkd({'lan.network': '''[Match] +MACAddress=11:22:33:44:55:66 +Name=lan +Type=!vlan bond bridge + +[Link] +MTUBytes=9000 + +[Network] +LinkLocalAddressing=ipv6 +VLAN=vlan20 +''', + 'lan.link': '''[Match] +MACAddress=11:22:33:44:55:66 +Type=!vlan bond bridge + +[Link] +Name=lan +WakeOnLan=off +MTUBytes=9000 +''', + 'vlan20.network': ND_EMPTY % ('vlan20', 'ipv6'), + 'vlan20.netdev': ND_VLAN % ('vlan20', 20)}) + + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=mac:11:22:33:44:55:66,interface-name:lan,interface-name:vlan20,''') + self.assert_nm_udev(None) + + +class TestNetworkManager(TestBase): + + def test_vlan(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + en1: {} + vlans: + enblue: + id: 1 + link: en1 + addresses: [1.2.3.4/24] + engreen: {id: 2, link: en1, dhcp6: true}''') + + self.assert_networkd({}) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'enblue': '''[connection] +id=netplan-enblue +type=vlan +interface-name=enblue + +[vlan] +id=1 +parent=en1 + +[ipv4] +method=manual +address1=1.2.3.4/24 + +[ipv6] +method=ignore +''', + 'engreen': '''[connection] +id=netplan-engreen +type=vlan +interface-name=engreen + +[vlan] +id=2 +parent=en1 + +[ipv4] +method=link-local + +[ipv6] +method=auto +'''}) + self.assert_nm_udev(None) + + def test_vlan_parent_match(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + en-v: + match: {macaddress: "11:22:33:44:55:66"} + vlans: + engreen: {id: 2, link: en-v, dhcp4: true}''') + + self.assert_networkd({}) + + # get assigned UUID from en-v connection + with open(os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/netplan-en-v.nmconnection')) as f: + m = re.search('uuid=([0-9a-fA-F-]{36})\n', f.read()) + self.assertTrue(m) + uuid = m.group(1) + self.assertNotEquals(uuid, "00000000-0000-0000-0000-000000000000") + + self.assert_nm({'en-v': '''[connection] +id=netplan-en-v +type=ethernet +uuid=%s + +[ethernet] +wake-on-lan=0 +mac-address=11:22:33:44:55:66 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''' % uuid, + 'engreen': '''[connection] +id=netplan-engreen +type=vlan +interface-name=engreen + +[vlan] +id=2 +parent=%s + +[ipv4] +method=auto + +[ipv6] +method=ignore +''' % uuid}) + self.assert_nm_udev(None) + + def test_vlan_sriov(self): + # we need to make sure renderer: sriov vlans are not saved as part of + # the NM/networkd config + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + en1: {} + vlans: + enblue: + id: 1 + link: en1 + addresses: [1.2.3.4/24] + renderer: sriov + engreen: {id: 2, link: en1, dhcp6: true}''') + + self.assert_networkd({}) + self.assert_nm({'en1': '''[connection] +id=netplan-en1 +type=ethernet +interface-name=en1 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +''', + 'engreen': '''[connection] +id=netplan-engreen +type=vlan +interface-name=engreen + +[vlan] +id=2 +parent=en1 + +[ipv4] +method=link-local + +[ipv6] +method=auto +'''}) + self.assert_nm_udev(None) diff --git a/tests/generator/test_wifis.py b/tests/generator/test_wifis.py new file mode 100644 index 0000000..513d788 --- /dev/null +++ b/tests/generator/test_wifis.py @@ -0,0 +1,692 @@ +# +# Tests for VLAN devices config generated via netplan +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import stat + +from .base import TestBase, ND_WIFI_DHCP4 + + +class TestNetworkd(TestBase): + + def test_wifi(self): + self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "s0s3kr1t" + bssid: 00:11:22:33:44:55 + band: 2.4GHz + channel: 11 + workplace: + password: "c0mpany1" + bssid: de:ad:be:ef:ca:fe + band: 5GHz + channel: 100 + peer2peer: + mode: adhoc + hidden-y: + hidden: y + password: "0bscur1ty" + hidden-n: + hidden: n + password: "5ecur1ty" + channel-no-band: + channel: 7 + band-no-channel: + band: 2.4G + band-no-channel2: + band: 5G + dhcp4: yes''') + + self.assert_networkd({'wl0.network': ND_WIFI_DHCP4 % 'wl0'}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:wl0,''') + self.assert_nm_udev(None) + + # generates wpa config and enables wpasupplicant unit + with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: + new_config = f.read() + + network = 'ssid="{}"\n freq_list='.format('band-no-channel2') + freqs_5GHz = [5610, 5310, 5620, 5320, 5630, 5640, 5340, 5035, 5040, 5045, 5055, 5060, 5660, 5680, 5670, 5080, 5690, + 5700, 5710, 5720, 5825, 5745, 5755, 5805, 5765, 5160, 5775, 5170, 5480, 5180, 5795, 5190, 5500, 5200, + 5510, 5210, 5520, 5220, 5530, 5230, 5540, 5240, 5550, 5250, 5560, 5260, 5570, 5270, 5580, 5280, 5590, + 5290, 5600, 5300, 5865, 5845, 5785] + freqs = new_config.split(network) + freqs = freqs[1].split('\n')[0] + self.assertEqual(len(freqs.split(' ')), len(freqs_5GHz)) + for freq in freqs_5GHz: + self.assertRegexpMatches(new_config, '{}[ 0-9]*{}[ 0-9]*\n'.format(network, freq)) + + network = 'ssid="{}"\n freq_list='.format('band-no-channel') + freqs_24GHz = [2412, 2417, 2422, 2427, 2432, 2442, 2447, 2437, 2452, 2457, 2462, 2467, 2472, 2484] + freqs = new_config.split(network) + freqs = freqs[1].split('\n')[0] + self.assertEqual(len(freqs.split(' ')), len(freqs_24GHz)) + for freq in freqs_24GHz: + self.assertRegexpMatches(new_config, '{}[ 0-9]*{}[ 0-9]*\n'.format(network, freq)) + + self.assertIn(''' +network={ + ssid="channel-no-band" + key_mgmt=NONE +} +''', new_config) + self.assertIn(''' +network={ + ssid="peer2peer" + mode=1 + key_mgmt=NONE +} +''', new_config) + self.assertIn(''' +network={ + ssid="hidden-y" + scan_ssid=1 + key_mgmt=WPA-PSK + psk="0bscur1ty" +} +''', new_config) + self.assertIn(''' +network={ + ssid="hidden-n" + key_mgmt=WPA-PSK + psk="5ecur1ty" +} +''', new_config) + self.assertIn(''' +network={ + ssid="workplace" + bssid=de:ad:be:ef:ca:fe + freq_list=5500 + key_mgmt=WPA-PSK + psk="c0mpany1" +} +''', new_config) + self.assertIn(''' +network={ + ssid="Joe's Home" + bssid=00:11:22:33:44:55 + freq_list=2462 + key_mgmt=WPA-PSK + psk="s0s3kr1t" +} +''', new_config) + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + + def test_wifi_upgrade(self): + # pretend an old 'netplan-wpa@*.service' link still exists on an upgraded system + os.makedirs(os.path.join(self.workdir.name, 'lib/systemd/system')) + os.makedirs(os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants')) + with open(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), 'w') as out: + out.write('''[Unit] +Description=WPA supplicant for netplan %I +DefaultDependencies=no +Requires=sys-subsystem-net-devices-%i.device +After=sys-subsystem-net-devices-%i.device +Before=network.target +Wants=network.target + +[Service] +Type=simple +ExecStart=/sbin/wpa_supplicant -c /run/netplan/wpa-%I.conf -i%I''') + os.symlink(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), + os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service')) + + # run generate, which should cleanup the old files/symlinks + self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + "Joe's Home": + password: "s0s3kr1t" + dhcp4: yes''') + + # verify new files/links exist, while old have been removed + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + # old files/links + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'))) + self.assertFalse(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service'))) + + # pretend another old systemd service file exists for wl1 + os.symlink(os.path.join(self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'), + os.path.join(self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl1.service')) + + # run generate again, to verify the historical netplan-wpa@.service links and wl0 links are gone + self.generate('''network: + version: 2 + wifis: + wl1: + access-points: + "Other Home": + password: "s0s3kr1t" + dhcp4: yes''') + + # verify new files/links exist, while old have been removed + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl1.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl1.service'))) + # old files/links + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'lib/systemd/system/netplan-wpa@.service'))) + self.assertFalse(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl1.service'))) + self.assertFalse(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa@wl0.service'))) + self.assertFalse(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) + self.assertFalse(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + + def test_wifi_route(self): + self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + password: "c0mpany1" + dhcp4: yes + routes: + - to: 10.10.10.0/24 + via: 8.8.8.8''') + + self.assert_networkd({'wl0.network': '''[Match] +Name=wl0 + +[Network] +DHCP=ipv4 +LinkLocalAddressing=ipv6 + +[Route] +Destination=10.10.10.0/24 +Gateway=8.8.8.8 + +[DHCP] +RouteMetric=600 +UseMTU=true +'''}) + + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:wl0,''') + self.assert_nm_udev(None) + + def test_wifi_match(self): + err = self.generate('''network: + version: 2 + wifis: + somewifi: + match: + driver: foo + access-points: + workplace: + password: "c0mpany1" + dhcp4: yes''', expect_fail=True) + self.assertIn('networkd backend does not support wifi with match:', err) + + def test_wifi_ap(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + access-points: + workplace: + password: "c0mpany1" + mode: ap + dhcp4: yes''', expect_fail=True) + self.assertIn('wl0: workplace: networkd does not support this wifi mode', err) + + def test_wifi_wowlan(self): + self.generate('''network: + version: 2 + wifis: + wl0: + wakeonwlan: + - any + - disconnect + - magic_pkt + - gtk_rekey_failure + - eap_identity_req + - four_way_handshake + - rfkill_release + access-points: + homenet: {mode: infrastructure}''') + + self.assert_networkd({'wl0.network': '''[Match] +Name=wl0 + +[Network] +LinkLocalAddressing=ipv6 +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:wl0,''') + self.assert_nm_udev(None) + + # generates wpa config and enables wpasupplicant unit + with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: + new_config = f.read() + self.assertIn(''' +wowlan_triggers=any disconnect magic_pkt gtk_rekey_failure eap_identity_req four_way_handshake rfkill_release +network={ + ssid="homenet" + key_mgmt=NONE +} +''', new_config) + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + + def test_wifi_wowlan_default(self): + self.generate('''network: + version: 2 + wifis: + wl0: + wakeonwlan: [default] + access-points: + homenet: {mode: infrastructure}''') + + self.assert_networkd({'wl0.network': '''[Match] +Name=wl0 + +[Network] +LinkLocalAddressing=ipv6 +'''}) + self.assert_nm(None, '''[keyfile] +# devices managed by networkd +unmanaged-devices+=interface-name:wl0,''') + self.assert_nm_udev(None) + + # generates wpa config and enables wpasupplicant unit + with open(os.path.join(self.workdir.name, 'run/netplan/wpa-wl0.conf')) as f: + new_config = f.read() + self.assertIn(''' +network={ + ssid="homenet" + key_mgmt=NONE +} +''', new_config) + self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o600) + self.assertTrue(os.path.isfile(os.path.join( + self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service'))) + self.assertTrue(os.path.islink(os.path.join( + self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service'))) + + +class TestNetworkManager(TestBase): + + def test_wifi_default(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + "Joe's Home": + password: "s0s3kr1t" + bssid: 00:11:22:33:44:55 + band: 2.4GHz + channel: 11 + workplace: + password: "c0mpany1" + bssid: de:ad:be:ef:ca:fe + band: 5GHz + channel: 100 + hidden-y: + hidden: y + password: "0bscur1ty" + hidden-n: + hidden: n + password: "5ecur1ty" + channel-no-band: + channel: 22 + band-no-channel: + band: 5GHz + dhcp4: yes''') + + self.assert_nm({'wl0-Joe%27s%20Home': '''[connection] +id=netplan-wl0-Joe's Home +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=Joe's Home +mode=infrastructure +bssid=00:11:22:33:44:55 +band=bg +channel=11 + +[wifi-security] +key-mgmt=wpa-psk +psk=s0s3kr1t +''', + 'wl0-workplace': '''[connection] +id=netplan-wl0-workplace +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=workplace +mode=infrastructure +bssid=de:ad:be:ef:ca:fe +band=a +channel=100 + +[wifi-security] +key-mgmt=wpa-psk +psk=c0mpany1 +''', + 'wl0-hidden-y': '''[connection] +id=netplan-wl0-hidden-y +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=hidden-y +mode=infrastructure +hidden=true + +[wifi-security] +key-mgmt=wpa-psk +psk=0bscur1ty +''', + 'wl0-hidden-n': '''[connection] +id=netplan-wl0-hidden-n +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=hidden-n +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-psk +psk=5ecur1ty +''', + 'wl0-channel-no-band': '''[connection] +id=netplan-wl0-channel-no-band +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=channel-no-band +mode=infrastructure +''', + 'wl0-band-no-channel': '''[connection] +id=netplan-wl0-band-no-channel +type=wifi +interface-name=wl0 + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=band-no-channel +mode=infrastructure +band=a +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_wifi_match_mac(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + all: + match: + macaddress: 11:22:33:44:55:66 + access-points: + workplace: {}''') + + self.assert_nm({'all-workplace': '''[connection] +id=netplan-all-workplace +type=wifi + +[wifi] +mac-address=11:22:33:44:55:66 +ssid=workplace +mode=infrastructure + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_wifi_match_all(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + all: + match: {} + access-points: + workplace: {mode: infrastructure}''') + + self.assert_nm({'all-workplace': '''[connection] +id=netplan-all-workplace +type=wifi + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=workplace +mode=infrastructure +'''}) + + def test_wifi_ap(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + homenet: + mode: ap + password: s0s3cret''') + + self.assert_nm({'wl0-homenet': '''[connection] +id=netplan-wl0-homenet +type=wifi +interface-name=wl0 + +[ipv4] +method=shared + +[ipv6] +method=ignore + +[wifi] +ssid=homenet +mode=ap + +[wifi-security] +key-mgmt=wpa-psk +psk=s0s3cret +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) + + def test_wifi_adhoc(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + access-points: + homenet: + mode: adhoc''') + + self.assert_nm({'wl0-homenet': '''[connection] +id=netplan-wl0-homenet +type=wifi +interface-name=wl0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=homenet +mode=adhoc +'''}) + + def test_wifi_wowlan(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + wakeonwlan: [any, tcp, four_way_handshake, magic_pkt] + access-points: + homenet: {mode: infrastructure}''') + + self.assert_nm({'wl0-homenet': '''[connection] +id=netplan-wl0-homenet +type=wifi +interface-name=wl0 + +[wifi] +wake-on-wlan=330 +ssid=homenet +mode=infrastructure + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_wifi_wowlan_default(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + wifis: + wl0: + wakeonwlan: [default] + access-points: + homenet: {mode: infrastructure}''') + + self.assert_nm({'wl0-homenet': '''[connection] +id=netplan-wl0-homenet +type=wifi +interface-name=wl0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=homenet +mode=infrastructure +'''}) + + +class TestConfigErrors(TestBase): + + def test_wifi_invalid_wowlan(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + wakeonwlan: [bogus] + access-points: + homenet: {mode: infrastructure}''', expect_fail=True) + self.assertIn("Error in network definition: invalid value for wakeonwlan: 'bogus'", err) + + def test_wifi_wowlan_unsupported(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + wakeonwlan: [tcp] + access-points: + homenet: {mode: infrastructure}''', expect_fail=True) + self.assertIn("ERROR: unsupported wowlan_triggers mask: 0x100", err) + + def test_wifi_wowlan_exclusive(self): + err = self.generate('''network: + version: 2 + wifis: + wl0: + wakeonwlan: [default, magic_pkt] + access-points: + homenet: {mode: infrastructure}''', expect_fail=True) + self.assertIn("Error in network definition: 'default' is an exclusive flag for wakeonwlan", err) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..be79e88 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1,17 @@ +# +# Integration tests. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/tests/integration/base.py b/tests/integration/base.py new file mode 100644 index 0000000..5042bf4 --- /dev/null +++ b/tests/integration/base.py @@ -0,0 +1,484 @@ +# +# System integration tests of netplan-generate. NM and networkd are +# started on the generated configuration, using emulated ethernets (veth) and +# Wifi (mac80211-hwsim). These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Martin Pitt +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import re +import time +import subprocess +import tempfile +import unittest +import shutil +import gi +import glob + +# make sure we point to libnetplan properly. +os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) + +test_backends = "networkd NetworkManager" if "NETPLAN_TEST_BACKENDS" not in os.environ else os.environ["NETPLAN_TEST_BACKENDS"] + +for program in ['wpa_supplicant', 'hostapd', 'dnsmasq']: + if subprocess.call(['which', program], stdout=subprocess.PIPE) != 0: + sys.stderr.write('%s is required for this test suite, but not available. Skipping\n' % program) + sys.exit(0) + +nm_uses_dnsmasq = b'dns=dnsmasq' in subprocess.check_output(['NetworkManager', '--print-config']) + + +def resolved_in_use(): + return os.path.isfile('/run/systemd/resolve/resolv.conf') + + +class IntegrationTestsBase(unittest.TestCase): + '''Common functionality for network test cases + + setUp() creates two test ethernet devices (self.dev_e_{ap,client} and + self.dev_e2_{ap,client}. + + Each test should call self.setup_eth() with the desired configuration. + ''' + @classmethod + def setUpClass(klass): + shutil.rmtree('/etc/netplan', ignore_errors=True) + os.makedirs('/etc/netplan', exist_ok=True) + # Try to keep autopkgtest's management network (eth0/ens3) up and + # configured. It should be running all the time, independently of netplan + os.makedirs('/etc/systemd/network', exist_ok=True) + with open('/etc/systemd/network/20-wired.network', 'w') as f: + f.write('[Match]\nName=eth0 en*\n\n[Network]\nDHCP=ipv4') + + # ensure NM can manage our fake eths + os.makedirs('/run/udev/rules.d', exist_ok=True) + with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: + f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') + subprocess.check_call(['udevadm', 'control', '--reload']) + + os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) + with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: + f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') + subprocess.check_call(['netplan', 'apply']) + subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) + + @classmethod + def tearDownClass(klass): + try: + os.remove('/run/NetworkManager/conf.d/test-blacklist.conf') + except FileNotFoundError: + pass + try: + os.remove('/run/udev/rules.d/99-nm-veth-test.rules') + except FileNotFoundError: + pass + + def tearDown(self): + subprocess.call(['systemctl', 'stop', 'NetworkManager', 'systemd-networkd', 'netplan-wpa-*', + 'netplan-ovs-*', 'systemd-networkd.socket']) + # NM has KillMode=process and leaks dhclient processes + subprocess.call(['systemctl', 'kill', 'NetworkManager']) + subprocess.call(['systemctl', 'reset-failed', 'NetworkManager', 'systemd-networkd'], + stderr=subprocess.DEVNULL) + shutil.rmtree('/etc/netplan', ignore_errors=True) + shutil.rmtree('/run/NetworkManager', ignore_errors=True) + shutil.rmtree('/run/systemd/network', ignore_errors=True) + for f in glob.glob('/run/systemd/system/netplan-*'): + os.remove(f) + for f in glob.glob('/run/systemd/system/**/netplan-*'): + os.remove(f) + subprocess.call(['systemctl', 'daemon-reload']) + try: + os.remove('/run/systemd/generator/netplan.stamp') + except FileNotFoundError: + pass + # Keep the management network (eth0/ens3 from 20-wired.network) up + subprocess.check_call(['systemctl', 'restart', 'systemd-networkd']) + + @classmethod + def create_devices(klass): + '''Create Access Point and Client devices with veth''' + + if os.path.exists('/sys/class/net/eth42'): + raise SystemError('eth42 interface already exists') + + # create virtual ethernet devs + subprocess.check_call(['ip', 'link', 'add', 'name', 'eth42', 'type', + 'veth', 'peer', 'name', 'veth42']) + klass.dev_e_ap = 'veth42' + klass.dev_e_client = 'eth42' + klass.dev_e_ap_ip4 = '192.168.5.1/24' + klass.dev_e_ap_ip6 = '2600::1/64' + subprocess.check_call(['ip', 'link', 'add', 'name', 'eth43', 'type', + 'veth', 'peer', 'name', 'veth43']) + klass.dev_e2_ap = 'veth43' + klass.dev_e2_client = 'eth43' + klass.dev_e2_ap_ip4 = '192.168.6.1/24' + klass.dev_e2_ap_ip6 = '2601::1/64' + # Creation of the veths introduces a race with newer versions of + # systemd, as it will change the initial MAC address after the device + # was created and networkd took control. Give it some time, so we read + # the correct MAC address + time.sleep(0.1) + out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth42'], + universal_newlines=True) + klass.dev_e_client_mac = out.split()[2] + out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth43'], + universal_newlines=True) + klass.dev_e2_client_mac = out.split()[2] + + os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) + + # work around https://launchpad.net/bugs/1615044 + with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: + f.write('[keyfile]\nunmanaged-devices=') + + @classmethod + def shutdown_devices(klass): + '''Remove test devices''' + + subprocess.check_call(['ip', 'link', 'del', 'dev', klass.dev_e_ap]) + subprocess.check_call(['ip', 'link', 'del', 'dev', klass.dev_e2_ap]) + klass.dev_e_ap = None + klass.dev_e_client = None + klass.dev_e2_ap = None + klass.dev_e2_client = None + + subprocess.call(['ip', 'link', 'del', 'dev', 'mybr'], + stderr=subprocess.PIPE) + + def setUp(self): + '''Create test devices and workdir''' + + self.create_devices() + self.addCleanup(self.shutdown_devices) + self.workdir_obj = tempfile.TemporaryDirectory() + self.workdir = self.workdir_obj.name + self.config = '/etc/netplan/01-main.yaml' + os.makedirs('/etc/netplan', exist_ok=True) + + # create static entropy file to avoid draining/blocking on /dev/random + self.entropy_file = os.path.join(self.workdir, 'entropy') + with open(self.entropy_file, 'wb') as f: + f.write(b'012345678901234567890') + + def setup_eth(self, ipv6_mode, start_dnsmasq=True): + '''Set up simulated ethernet router + + On self.dev_e_ap, run dnsmasq according to ipv6_mode, see + start_dnsmasq(). + + This is torn down automatically at the end of the test. + ''' + # give our router an IP + subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_e_ap]) + subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_e2_ap]) + if ipv6_mode is not None: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip6, 'dev', self.dev_e_ap]) + subprocess.check_call(['ip', 'a', 'add', self.dev_e2_ap_ip6, 'dev', self.dev_e2_ap]) + else: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip4, 'dev', self.dev_e_ap]) + subprocess.check_call(['ip', 'a', 'add', self.dev_e2_ap_ip4, 'dev', self.dev_e2_ap]) + subprocess.check_call(['ip', 'link', 'set', self.dev_e_ap, 'up']) + subprocess.check_call(['ip', 'link', 'set', self.dev_e2_ap, 'up']) + if start_dnsmasq: + self.start_dnsmasq(ipv6_mode, self.dev_e_ap) + self.start_dnsmasq(ipv6_mode, self.dev_e2_ap) + + # + # Internal implementation details + # + + @classmethod + def poll_text(klass, logpath, string, timeout=50): + '''Poll log file for a given string with a timeout. + + Timeout is given in deciseconds. + ''' + log = '' + while timeout > 0: + if os.path.exists(logpath): + break + timeout -= 1 + time.sleep(0.1) + assert timeout > 0, 'Timed out waiting for file %s to appear' % logpath + + with open(logpath) as f: + while timeout > 0: + line = f.readline() + if line: + log += line + if string in line: + break + continue + timeout -= 1 + time.sleep(0.1) + + assert timeout > 0, 'Timed out waiting for "%s":\n------------\n%s\n-------\n' % (string, log) + + def start_dnsmasq(self, ipv6_mode, iface): + '''Start dnsmasq. + + If ipv6_mode is None, IPv4 is set up with DHCP. If it is not None, it + must be a valid dnsmasq mode, i. e. a combination of "ra-only", + "slaac", "ra-stateless", and "ra-names". See dnsmasq(8). + ''' + if ipv6_mode is None: + if iface == self.dev_e2_ap: + dhcp_range = '192.168.6.10,192.168.6.200' + else: + dhcp_range = '192.168.5.10,192.168.5.200' + else: + if iface == self.dev_e2_ap: + dhcp_range = '2601::10,2601::20' + else: + dhcp_range = '2600::10,2600::20' + if ipv6_mode: + dhcp_range += ',' + ipv6_mode + + dnsmasq_log = os.path.join(self.workdir, 'dnsmasq-%s.log' % iface) + lease_file = os.path.join(self.workdir, 'dnsmasq-%s.leases' % iface) + + p = subprocess.Popen(['dnsmasq', '--keep-in-foreground', '--log-queries', + '--log-facility=' + dnsmasq_log, + '--conf-file=/dev/null', + '--dhcp-leasefile=' + lease_file, + '--bind-interfaces', + '--interface=' + iface, + '--except-interface=lo', + '--enable-ra', + '--dhcp-range=' + dhcp_range]) + self.addCleanup(p.kill) + + if ipv6_mode is not None: + self.poll_text(dnsmasq_log, 'IPv6 router advertisement enabled') + else: + self.poll_text(dnsmasq_log, 'DHCP, IP range') + + def assert_iface(self, iface, expected_ip_a=None, unexpected_ip_a=None): + '''Assert that client interface has been created''' + + out = subprocess.check_output(['ip', 'a', 'show', 'dev', iface], + universal_newlines=True) + if expected_ip_a: + for r in expected_ip_a: + self.assertRegex(out, r, out) + if unexpected_ip_a: + for r in unexpected_ip_a: + self.assertNotRegex(out, r, out) + + return out + + def assert_iface_up(self, iface, expected_ip_a=None, unexpected_ip_a=None): + '''Assert that client interface is up''' + + out = self.assert_iface(iface, expected_ip_a, unexpected_ip_a) + if 'bond' not in iface: + self.assertIn('state UP', out) + + def generate_and_settle(self, wait_interfaces=None): + '''Generate config, launch and settle NM and networkd''' + + # regenerate netplan config + out = subprocess.check_output(['netplan', 'apply'], stderr=subprocess.STDOUT, universal_newlines=True) + if 'Run \'systemctl daemon-reload\' to reload units.' in out: + self.fail('systemd units changed without reload') + # start NM so that we can verify that it does not manage anything + subprocess.check_call(['systemctl', 'start', 'NetworkManager.service']) + + # Wait for interfaces to be ready: + ifaces = wait_interfaces if wait_interfaces is not None else [self.dev_e_client, self.dev_e2_client] + for iface_state in ifaces: + split = iface_state.split('/', 1) + iface = split[0] + state = split[1] if len(split) > 1 else None + print(iface, end=' ', flush=True) + if self.backend == 'NetworkManager': + self.nm_wait_connected(iface, 60) + else: + self.networkd_wait_connected(iface, 60) + # wait for iproute2 state change + if state: + self.wait_output(['ip', 'addr', 'show', iface], state, 30) + + def state(self, iface, state): + '''Tell generate_and_settle() to wait for a specific state''' + return iface + '/' + state + + def state_dhcp4(self, iface): + '''Tell generate_and_settle() to wait for assignment of an IP4 address from DHCP''' + return self.state(iface, 'inet 192.168.') # TODO: make this a regex to check for specific DHCP ranges + + def state_dhcp6(self, iface): + '''Tell generate_and_settle() to wait for assignment of an IP6 address from DHCP''' + return self.state(iface, 'inet6 260') # TODO: make this a regex to check for specific DHCP ranges + + def nm_online_full(self, iface, timeout=60): + '''Wait for NetworkManager connection to be completed (incl. IP4 & DHCP)''' + + gi.require_version('NM', '1.0') + from gi.repository import NM + for t in range(timeout): + c = NM.Client.new(None) + con = c.get_device_by_iface(iface).get_active_connection() + if not con: + self.fail('no active connection for %s by NM' % iface) + flags = NM.utils_enum_to_str(NM.ActivationStateFlags, con.get_state_flags()) + if "ip4-ready" in flags: + break + time.sleep(1) + else: + self.fail('timed out waiting for %s to get ready by NM' % iface) + + def wait_output(self, cmd, expected_output, timeout=10): + for _ in range(timeout): + try: + out = subprocess.check_output(cmd, universal_newlines=True) + except subprocess.CalledProcessError: + out = '' + if expected_output in out: + break + sys.stdout.write('.') # waiting indicator + time.sleep(1) + else: + subprocess.call(cmd) # print output of the failed command + self.fail('timed out waiting for "{}" to appear in {}'.format(expected_output, cmd)) + + def nm_wait_connected(self, iface, timeout=10): + self.wait_output(['nmcli', 'dev', 'show', iface], '(connected', timeout) + + def networkd_wait_connected(self, iface, timeout=10): + # "State: routable (configured)" or "State: degraded (configured)" + self.wait_output(['networkctl', 'status', iface], '(configured', timeout) + + @classmethod + def is_active(klass, unit): + '''Check if given unit is active or activating''' + + p = subprocess.Popen(['systemctl', 'is-active', unit], stdout=subprocess.PIPE) + out = p.communicate()[0] + return p.returncode == 0 or out.startswith(b'activating') + + +class IntegrationTestsWifi(IntegrationTestsBase): + '''Common functionality for network test cases + + setUp() creates two test wlan devices, one for a simulated access point + (self.dev_w_ap), the other for a simulated client device + (self.dev_w_client), and two test ethernet devices (self.dev_e_{ap,client} + and self.dev_e2_{ap,client}. + + Each test should call self.setup_ap() or self.setup_eth() with the desired + configuration. + ''' + @classmethod + def setUpClass(klass): + super().setUpClass() + # ensure we have this so that iw works + try: + subprocess.check_call(['modprobe', 'cfg80211']) + # set regulatory domain "EU", so that we can use 80211.a 5 GHz channels + out = subprocess.check_output(['iw', 'reg', 'get'], universal_newlines=True) + m = re.match(r'^(?:global\n)?country (\S+):', out) + assert m + klass.orig_country = m.group(1) + subprocess.check_call(['iw', 'reg', 'set', 'EU']) + except Exception: + raise unittest.SkipTest("cfg80211 (wireless) is unavailable, can't test") + + @classmethod + def tearDownClass(klass): + subprocess.check_call(['iw', 'reg', 'set', klass.orig_country]) + super().tearDownClass() + + @classmethod + def create_devices(klass): + '''Create Access Point and Client devices with mac80211_hwsim and veth''' + if os.path.exists('/sys/module/mac80211_hwsim'): + raise SystemError('mac80211_hwsim module already loaded') + super().create_devices() + # create virtual wlan devs + before_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) + subprocess.check_call(['modprobe', 'mac80211_hwsim']) + # wait 5 seconds for fake devices to appear + timeout = 50 + while timeout > 0: + after_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) + if len(after_wlan) - len(before_wlan) >= 2: + break + timeout -= 1 + time.sleep(0.1) + else: + raise SystemError('timed out waiting for fake devices to appear') + + devs = list(after_wlan - before_wlan) + klass.dev_w_ap = devs[0] + klass.dev_w_client = devs[1] + + # don't let NM trample over our fake AP + with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + + @classmethod + def shutdown_devices(klass): + '''Remove test devices''' + super().shutdown_devices() + klass.dev_w_ap = None + klass.dev_w_client = None + subprocess.check_call(['rmmod', 'mac80211_hwsim']) + + def start_hostapd(self, conf): + hostapd_conf = os.path.join(self.workdir, 'hostapd.conf') + with open(hostapd_conf, 'w') as f: + f.write('interface=%s\ndriver=nl80211\n' % self.dev_w_ap) + f.write(conf) + + log = os.path.join(self.workdir, 'hostapd.log') + p = subprocess.Popen(['hostapd', '-e', self.entropy_file, '-f', log, hostapd_conf], + stdout=subprocess.PIPE) + self.addCleanup(p.wait) + self.addCleanup(p.terminate) + self.poll_text(log, '' + self.dev_w_ap + ': AP-ENABLED', 500) + + def setup_ap(self, hostapd_conf, ipv6_mode): + '''Set up simulated access point + + On self.dev_w_ap, run hostapd with given configuration. Setup dnsmasq + according to ipv6_mode, see start_dnsmasq(). + + This is torn down automatically at the end of the test. + ''' + # give our AP an IP + subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_w_ap]) + if ipv6_mode is not None: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip6, 'dev', self.dev_w_ap]) + else: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip4, 'dev', self.dev_w_ap]) + self.start_hostapd(hostapd_conf) + self.start_dnsmasq(ipv6_mode, self.dev_w_ap) + + def assert_iface_up(self, iface, expected_ip_a=None, unexpected_ip_a=None): + '''Assert that client interface is up''' + super().assert_iface_up(iface, expected_ip_a, unexpected_ip_a) + if iface == self.dev_w_client: + out = subprocess.check_output(['iw', 'dev', iface, 'link'], + universal_newlines=True) + # self.assertIn('Connected to ' + self.mac_w_ap, out) + self.assertIn('SSID: fake net', out) diff --git a/tests/integration/bonds.py b/tests/integration/bonds.py new file mode 100644 index 0000000..763f7e5 --- /dev/null +++ b/tests/integration/bonds.py @@ -0,0 +1,675 @@ +#!/usr/bin/python3 +# +# Integration tests for bonds +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_bond_base(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + + def test_bond_primary_slave(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: {} + %(e2c)s: {} + bonds: + mybond: + interfaces: [%(ec)s, %(e2c)s] + parameters: + mode: active-backup + primary: %(ec)s + addresses: [ '10.10.10.1/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'mybond']) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up(self.dev_e2_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 10.10.10.1/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + result = f.read().strip() + self.assertIn(self.dev_e_client, result) + self.assertIn(self.dev_e2_client, result) + with open('/sys/class/net/mybond/bonding/primary') as f: + self.assertEqual(f.read().strip(), '%(ec)s' % {'ec': self.dev_e_client}) + + def test_bond_all_slaves_active(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + all-slaves-active: true + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/all_slaves_active') as f: + self.assertEqual(f.read().strip(), '1') + + def test_bond_mode_8023ad(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + parameters: + mode: 802.3ad + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), '802.3ad 4') + + def test_bond_mode_8023ad_adselect(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + parameters: + mode: 802.3ad + ad-select: bandwidth + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/ad_select') as f: + self.assertEqual(f.read().strip(), 'bandwidth 1') + + def test_bond_mode_8023ad_lacp_rate(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + parameters: + mode: 802.3ad + lacp-rate: fast + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/lacp_rate') as f: + self.assertEqual(f.read().strip(), 'fast 1') + + def test_bond_mode_activebackup_failover_mac(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + parameters: + mode: active-backup + fail-over-mac-policy: follow + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'active-backup 1') + with open('/sys/class/net/mybond/bonding/fail_over_mac') as f: + self.assertEqual(f.read().strip(), 'follow 2') + + def test_bond_mode_balance_xor(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + parameters: + mode: balance-xor + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'balance-xor 2') + + def test_bond_mode_balance_rr(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + parameters: + mode: balance-rr + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'balance-rr 0') + + def test_bond_mode_balance_rr_pps(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + parameters: + mode: balance-rr + packets-per-slave: 15 + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'balance-rr 0') + with open('/sys/class/net/mybond/bonding/packets_per_slave') as f: + self.assertEqual(f.read().strip(), '15') + + def test_bond_resend_igmp(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} + bonds: + mybond: + addresses: [192.168.9.9/24] + interfaces: [ethbn, ethb2] + parameters: + mode: balance-rr + mii-monitor-interval: 50s + resend-igmp: 100 +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'mybond']) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up(self.dev_e2_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.9.9/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + result = f.read().strip() + self.assertIn(self.dev_e_client, result) + self.assertIn(self.dev_e2_client, result) + with open('/sys/class/net/mybond/bonding/resend_igmp') as f: + self.assertEqual(f.read().strip(), '100') + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_bond_mac(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: + name: %(ec)s + macaddress: %(ec_mac)s + bonds: + mybond: + interfaces: [ethbn] + macaddress: 00:01:02:03:04:05 + dhcp4: yes''' % {'r': self.backend, + 'ec': self.dev_e_client, + 'ec_mac': self.dev_e_client_mac}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24', '00:01:02:03:04:05']) + + def test_bond_down_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: active-backup + mii-monitor-interval: 5 + down-delay: 10s + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/downdelay') as f: + self.assertEqual(f.read().strip(), '10000') + + def test_bond_up_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: active-backup + mii-monitor-interval: 5 + up-delay: 10000 + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/updelay') as f: + self.assertEqual(f.read().strip(), '10000') + + def test_bond_arp_interval(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [ 192.168.5.1 ] + arp-interval: 50s + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_interval') as f: + self.assertEqual(f.read().strip(), '50000') + + def test_bond_arp_targets(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-interval: 50000 + arp-ip-targets: [ 192.168.5.1 ] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: + self.assertEqual(f.read().strip(), '192.168.5.1') + + def test_bond_arp_targets_many_lp1829264(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-interval: 50000 + arp-ip-targets: [ 192.168.5.1, 192.168.5.34 ] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: + result = f.read().strip() + self.assertIn('192.168.5.1', result) + self.assertIn('192.168.5.34', result) + + def test_bond_arp_all_targets(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [192.168.5.1] + arp-interval: 50000 + arp-all-targets: all + arp-validate: all + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_all_targets') as f: + self.assertEqual(f.read().strip(), 'all 1') + + def test_bond_arp_validate(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [192.168.5.1] + arp-interval: 50000 + arp-validate: all + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_validate') as f: + self.assertEqual(f.read().strip(), 'all 3') + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + @unittest.skip("NetworkManager does not support setting MAC for a bond") + def test_bond_mac(self): + pass + + def test_bond_down_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: active-backup + mii-monitor-interval: 5 + down-delay: 10000 + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/downdelay') as f: + self.assertEqual(f.read().strip(), '10000') + + def test_bond_up_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: active-backup + mii-monitor-interval: 5 + up-delay: 10000 + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/updelay') as f: + self.assertEqual(f.read().strip(), '10000') + + def test_bond_arp_interval(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [ 192.168.5.1 ] + arp-interval: 50000 + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_interval') as f: + self.assertEqual(f.read().strip(), '50000') + + def test_bond_arp_targets(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-interval: 50000 + arp-ip-targets: [ 192.168.5.1 ] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_ip_target') as f: + self.assertEqual(f.read().strip(), '192.168.5.1') + + def test_bond_arp_all_targets(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + interfaces: [ethbn] + parameters: + mode: balance-xor + arp-ip-targets: [192.168.5.1] + arp-interval: 50000 + arp-all-targets: all + arp-validate: all + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/arp_all_targets') as f: + self.assertEqual(f.read().strip(), 'all 1') + + def test_bond_mode_balance_tlb_learn_interval(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + bonds: + mybond: + parameters: + mode: balance-tlb + mii-monitor-interval: 5 + learn-packet-interval: 15 + interfaces: [ethbn] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) + self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertEqual(f.read().strip(), self.dev_e_client) + with open('/sys/class/net/mybond/bonding/mode') as f: + self.assertEqual(f.read().strip(), 'balance-tlb 5') + with open('/sys/class/net/mybond/bonding/lp_interval') as f: + self.assertEqual(f.read().strip(), '15') + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/bridges.py b/tests/integration/bridges.py new file mode 100644 index 0000000..b24e6e9 --- /dev/null +++ b/tests/integration/bridges.py @@ -0,0 +1,348 @@ +#!/usr/bin/python3 +# +# Integration tests for bridges +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_eth_and_bridge(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + dhcp4: yes + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.state_dhcp4(self.dev_e_client), + self.dev_e2_client, + self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + + # ensure that they do not get managed by NM for foreign backends + expected_state = (self.backend == 'NetworkManager') and 'connected' or 'unmanaged' + out = subprocess.check_output(['nmcli', 'dev'], universal_newlines=True) + for i in [self.dev_e_client, self.dev_e2_client, 'mybr']: + self.assertRegex(out, r'%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) + + def test_bridge_path_cost(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + parameters: + path-cost: + ethbr: 50 + stp: false + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + with open('/sys/class/net/mybr/brif/%s/path_cost' % self.dev_e2_client) as f: + self.assertEqual(f.read().strip(), '50') + + def test_bridge_ageing_time(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + parameters: + ageing-time: 21 + stp: false + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + with open('/sys/class/net/mybr/bridge/ageing_time') as f: + self.assertEqual(f.read().strip(), '2100') + + def test_bridge_max_age(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + parameters: + max-age: 12 + stp: false + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + with open('/sys/class/net/mybr/bridge/max_age') as f: + self.assertEqual(f.read().strip(), '1200') + + def test_bridge_hello_time(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + parameters: + hello-time: 1 + stp: false + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + with open('/sys/class/net/mybr/bridge/hello_time') as f: + self.assertEqual(f.read().strip(), '100') + + def test_bridge_forward_delay(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + parameters: + forward-delay: 10 + stp: false + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + with open('/sys/class/net/mybr/bridge/forward_delay') as f: + self.assertEqual(f.read().strip(), '1000') + + def test_bridge_stp_false(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + parameters: + hello-time: 100000 + max-age: 100000 + stp: false + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + with open('/sys/class/net/mybr/bridge/stp_state') as f: + self.assertEqual(f.read().strip(), '0') + + def test_bridge_port_priority(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + parameters: + port-priority: + ethbr: 42 + stp: false + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + with open('/sys/class/net/mybr/brif/%s/priority' % self.dev_e2_client) as f: + self.assertEqual(f.read().strip(), '42') + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_bridge_mac(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: + name: %(ec)s + macaddress: %(ec_mac)s + bridges: + br0: + interfaces: [ethbr] + macaddress: "00:01:02:03:04:05" + dhcp4: yes''' % {'r': self.backend, + 'ec': self.dev_e_client, + 'ec_mac': self.dev_e_client_mac}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4('br0')]) + self.assert_iface_up(self.dev_e_client, ['master br0'], ['inet ']) + self.assert_iface_up('br0', ['inet 192.168.5.[0-9]+/24', 'ether 00:01:02:03:04:05']) + + def test_bridge_anonymous(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr]''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, 'mybr']) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', [], ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + + def test_bridge_isolated(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + bridges: + mybr: + interfaces: [] + addresses: [10.10.10.10/24]''' % {'r': self.backend}) + self.generate_and_settle(['mybr']) + self.assert_iface('mybr', ['inet 10.10.10.10/24']) + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + @unittest.skip("NetworkManager does not support setting MAC for a bridge") + def test_bridge_mac(self): + pass + + def test_bridge_priority(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbr: + match: {name: %(e2c)s} + bridges: + mybr: + interfaces: [ethbr] + parameters: + priority: 16384 + stp: false + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e2_client, self.state_dhcp4('mybr')]) + self.assert_iface_up(self.dev_e2_client, ['master mybr'], ['inet ']) + self.assert_iface_up('mybr', ['inet 192.168.6.[0-9]+/24']) + lines = subprocess.check_output(['bridge', 'link', 'show', 'mybr'], + universal_newlines=True).splitlines() + self.assertEqual(len(lines), 1, lines) + self.assertIn(self.dev_e2_client, lines[0]) + with open('/sys/class/net/mybr/bridge/priority') as f: + self.assertEqual(f.read().strip(), '16384') + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py new file mode 100644 index 0000000..ce016da --- /dev/null +++ b/tests/integration/ethernets.py @@ -0,0 +1,336 @@ +#!/usr/bin/python3 +# +# Integration tests for ethernet devices and features common to all device +# types. +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# AUthor: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, nm_uses_dnsmasq, resolved_in_use, test_backends + + +class _CommonTests(): + + def test_eth_mtu(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + enmtus: + match: {name: %(e2c)s} + mtu: 1492 + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.state_dhcp4(self.dev_e2_client)]) + self.assert_iface_up(self.dev_e2_client, + ['inet 192.168.6.[0-9]+/24', 'mtu 1492']) + + def test_eth_mac(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'set', self.dev_e2_client, 'address', self.dev_e2_client_mac]) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + enmac: + match: {name: %(e2c)s} + macaddress: 00:01:02:03:04:05 + dhcp4: yes''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.state_dhcp4(self.dev_e2_client)]) + self.assert_iface_up(self.dev_e2_client, + ['inet 192.168.6.[0-9]+/24', 'ether 00:01:02:03:04:05']) + + # Supposed to fail if tested against NetworkManager < 1.14 + # Interface globbing was introduced as of NM 1.14+ + def test_eth_glob(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + englob: + match: {name: "eth?2"} + addresses: ["172.16.42.99/18", "1234:FFFF::42/64"] +''' % {'r': self.backend}) # globbing match on "eth42", i.e. self.dev_e_client + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 172.16.42.99/18', 'inet6 1234:ffff::42/64']) + + def test_manual_addresses(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: ["172.16.42.99/18", "1234:FFFF::42/64"] + dhcp4: yes + %(e2c)s: + addresses: ["172.16.1.2/24"] + gateway4: "172.16.1.1" + nameservers: + addresses: [172.1.2.3] + search: ["fakesuffix"] +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.state_dhcp4(self.dev_e_client), self.dev_e2_client]) + if self.backend == 'NetworkManager': + self.nm_online_full(self.dev_e_client) + self.assert_iface_up(self.dev_e_client, + ['inet 172.16.42.99/18', + 'inet6 1234:ffff::42/64', + 'inet 192.168.5.[0-9]+/24']) # from DHCP + self.assert_iface_up(self.dev_e2_client, + ['inet 172.16.1.2/24']) + + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertNotIn(b'default', + subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'default via 172.16.1.1', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e2_client])) + self.assertNotIn(b'default', + subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e2_client])) + + # ensure that they do not get managed by NM for foreign backends + expected_state = (self.backend == 'NetworkManager') and 'connected' or 'unmanaged' + out = subprocess.check_output(['nmcli', 'dev'], universal_newlines=True) + for i in [self.dev_e_client, self.dev_e2_client]: + self.assertRegex(out, r'%s\s+(ethernet|bridge)\s+%s' % (i, expected_state)) + + with open('/etc/resolv.conf') as f: + resolv_conf = f.read() + + if self.backend == 'NetworkManager' and nm_uses_dnsmasq: + sys.stdout.write('[NM with dnsmasq] ') + sys.stdout.flush() + self.assertRegex(resolv_conf, 'search.*fakesuffix') + # not easy to peek dnsmasq's brain, so check its logging + out = subprocess.check_output(['journalctl', '--quiet', '-tdnsmasq', '-ocat', '--since=-30s'], + universal_newlines=True) + self.assertIn('nameserver 172.1.2.3', out) + elif resolved_in_use(): + sys.stdout.write('[resolved] ') + sys.stdout.flush() + out = subprocess.check_output(['resolvectl', 'status'], universal_newlines=True) + self.assertIn('DNS Servers: 172.1.2.3', out) + self.assertIn('fakesuffix', out) + else: + sys.stdout.write('[/etc/resolv.conf] ') + sys.stdout.flush() + self.assertRegex(resolv_conf, 'search.*fakesuffix') + # /etc/resolve.conf often already has three nameserver entries + if 'nameserver 172.1.2.3' not in resolv_conf: + self.assertGreaterEqual(resolv_conf.count('nameserver'), 3) + + # change the addresses, make sure that "apply" does not leave leftovers + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: ["172.16.5.3/20", "9876:BBBB::11/70"] + gateway6: "9876:BBBB::1" + %(e2c)s: + addresses: ["172.16.7.2/30", "4321:AAAA::99/80"] + dhcp4: yes +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.state_dhcp4(self.dev_e2_client)]) + if self.backend == 'NetworkManager': + self.nm_online_full(self.dev_e2_client) + self.assert_iface_up(self.dev_e_client, + ['inet 172.16.5.3/20'], + ['inet 192.168.5', # old DHCP + 'inet 172.16.42', # old static IPv4 + 'inet6 1234']) # old static IPv6 + self.assert_iface_up(self.dev_e2_client, + ['inet 172.16.7.2/30', + 'inet6 4321:aaaa::99/80', + 'inet 192.168.6.[0-9]+/24'], # from DHCP + ['inet 172.16.1']) # old static IPv4 + + self.assertNotIn(b'default', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'via 9876:bbbb::1', + subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) + self.assertIn(b'default via 192.168.6.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e2_client])) + self.assertNotIn(b'default', + subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e2_client])) + + def test_dhcp6(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: yes + accept-ra: yes''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.state_dhcp6(self.dev_e_client)]) + self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], ['inet 192.168']) + + def test_ip6_token(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: yes + accept-ra: yes + ipv6-address-token: ::42''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.state_dhcp6(self.dev_e_client)]) + self.assert_iface_up(self.dev_e_client, ['inet6 2600::42/64']) + + def test_link_local_all(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + link-local: [ ipv4, ipv6 ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + # Verify IPv4 and IPv6 link local addresses are there + self.assert_iface(self.dev_e_client, ['inet6 fe80:', 'inet 169.254.']) + + def test_rename_interfaces(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + idx: + match: + name: %(ec)s + set-name: iface1 + addresses: [10.10.10.11/24] + idy: + match: + macaddress: %(e2c_mac)s + set-name: iface2 + addresses: [10.10.10.22/24] +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c_mac': self.dev_e2_client_mac}) + self.generate_and_settle(['iface1', 'iface2']) + self.assert_iface_up('iface1', ['inet 10.10.10.11']) + self.assert_iface_up('iface2', ['inet 10.10.10.22']) + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_eth_dhcp6_off(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: no + accept-ra: yes + addresses: [ '192.168.1.100/24' ] + %(e2c)s: {}''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], []) + + def test_eth_dhcp6_off_no_accept_ra(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: no + accept-ra: no + addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, [], ['inet6 2600:']) + + # TODO: implement link-local handling in NetworkManager backend and move this test into CommonTests() + def test_link_local_ipv4(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + link-local: [ ipv4 ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + # Verify IPv4 link local address is there, while IPv6 is not + self.assert_iface(self.dev_e_client, ['inet 169.254.'], ['inet6 fe80:']) + + # TODO: implement link-local handling in NetworkManager backend and move this test into CommonTests() + def test_link_local_ipv6(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + link-local: [ ipv6 ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + # Verify IPv6 link local address is there, while IPv4 is not + self.assert_iface(self.dev_e_client, ['inet6 fe80:'], ['inet 169.254.']) + + # TODO: implement link-local handling in NetworkManager backend and move this test into CommonTests() + def test_link_local_disabled(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: ["172.16.5.3/20", "9876:BBBB::11/70"] # needed to bring up the interface at all + link-local: []''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + # Verify IPv4 and IPv6 link local addresses are not there + self.assert_iface(self.dev_e_client, + ['inet6 9876:bbbb::11/70', 'inet 172.16.5.3/20'], + ['inet6 fe80:', 'inet 169.254.']) + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + @unittest.skip("NetworkManager does not disable accept_ra: bug LP: #1704210") + def test_eth_dhcp6_off(self): + self.setup_eth('slaac') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + %(ec)s: + dhcp6: no + addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, [], ['inet6 2600:']) + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/ovs.py b/tests/integration/ovs.py new file mode 100644 index 0000000..8a6f60d --- /dev/null +++ b/tests/integration/ovs.py @@ -0,0 +1,557 @@ +#!/usr/bin/python3 +# +# Integration tests for bonds +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2020-2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def _collect_ovs_settings(self, bridge0): + d = {} + d['show'] = subprocess.check_output(['ovs-vsctl', 'show']) + d['ssl'] = subprocess.check_output(['ovs-vsctl', 'get-ssl']) + # Get external-ids + for tbl in ('Open_vSwitch', 'Controller', 'Bridge', 'Port', 'Interface'): + cols = 'name,external-ids' + if tbl == 'Open_vSwitch': + cols = 'external-ids' + elif tbl == 'Controller': + cols = '_uuid,external-ids' + d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', + 'bare', '--no-headings', 'list', tbl]) + # Get other-config + for tbl in ('Open_vSwitch', 'Bridge', 'Port', 'Interface'): + cols = 'name,other-config' + if tbl == 'Open_vSwitch': + cols = 'other-config' + d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', + 'bare', '--no-headings', 'list', tbl]) + # Get bond settings + for col in ('bond_mode', 'lacp'): + d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', + '--no-headings', 'list', 'Port']) + # Get bridge settings + d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-fail-mode', bridge0]) + for col in ('mcast_snooping_enable', 'rstp_enable', 'protocols'): + d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', + '--no-headings', 'list', 'Bridge']) + # Get controller settings + d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-controller', bridge0]) + for col in ('connection_mode',): + d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '--columns=_uuid,%s' % col, '-f', 'csv', '-d', + 'bare', '--no-headings', 'list', 'Controller']) + return d + + def test_cleanup_interfaces(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) + with open(self.config, 'w') as f: + f.write('''network: + openvswitch: + ports: + - [patch0-1, patch1-0] + bridges: + ovs0: {interfaces: [patch0-1]} + ovs1: {interfaces: [patch1-0]}''') + self.generate_and_settle(['ovs0', 'ovs1']) + # Basic verification that the bridges/ports/interfaces are there in OVS + out = subprocess.check_output(['ovs-vsctl', 'show']) + self.assertIn(b' Bridge ovs0', out) + self.assertIn(b' Port patch0-1', out) + self.assertIn(b' Interface patch0-1', out) + self.assertIn(b' Bridge ovs1', out) + self.assertIn(b' Port patch1-0', out) + self.assertIn(b' Interface patch1-0', out) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: + %(ec)s: {addresses: ['1.2.3.4/24']}''' % {'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + # Verify that the netplan=true tagged bridges/ports have been cleaned up + out = subprocess.check_output(['ovs-vsctl', 'show']) + self.assertNotIn(b'Bridge ovs0', out) + self.assertNotIn(b'Port patch0-1', out) + self.assertNotIn(b'Interface patch0-1', out) + self.assertNotIn(b'Bridge ovs1', out) + self.assertNotIn(b'Port patch1-0', out) + self.assertNotIn(b'Interface patch1-0', out) + self.assert_iface_up(self.dev_e_client, ['inet 1.2.3.4/24']) + + def test_cleanup_patch_ports(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patchy']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: + %(ec)s: {addresses: [10.10.10.20/24]} + openvswitch: + ports: [[patch0-1, patch1-0]] + bonds: + bond0: {interfaces: [patch1-0, %(ec)s]} + bridges: + ovs0: {interfaces: [patch0-1, bond0]}''' % {'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, 'ovs0']) + # Basic verification that the bridges/ports/interfaces are there in OVS + out = subprocess.check_output(['ovs-vsctl', 'show']) + self.assertIn(b' Bridge ovs0', out) + self.assertIn(b' Port patch0-1\n Interface patch0-1\n type: patch', out) + self.assertIn(b' Port bond0', out) + self.assertIn(b' Interface patch1-0\n type: patch', out) + self.assertIn(b' Interface eth42', out) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: + %(ec)s: {addresses: [10.10.10.20/24]} + openvswitch: + ports: [[patchx, patchy]] + bonds: + bond0: {interfaces: [patchx, %(ec)s]} + bridges: + ovs1: {interfaces: [patchy, bond0]}''' % {'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, 'ovs1']) + # Verify that the netplan=true tagged patch ports have been cleaned up + # even though the containing bond0 port still exists (with new patch ports) + out = subprocess.check_output(['ovs-vsctl', 'show']) + self.assertIn(b' Bridge ovs1', out) + self.assertIn(b' Port patchy\n Interface patchy\n type: patch', out) + self.assertIn(b' Port bond0', out) + self.assertIn(b' Interface patchx\n type: patch', out) + self.assertIn(b' Interface eth42', out) + self.assertNotIn(b'Bridge ovs0', out) + self.assertNotIn(b'Port patch0-1', out) + self.assertNotIn(b'Interface patch0-1', out) + self.assertNotIn(b'Port patch1-0', out) + self.assertNotIn(b'Interface patch1-0', out) + + def test_bridge_vlan(self): + self.setup_eth(None, True) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-data']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + ethernets: + %(ec)s: + mtu: 9000 + bridges: + br-%(ec)s: + dhcp4: true + mtu: 9000 + interfaces: [%(ec)s] + openvswitch: {} + br-data: + openvswitch: {} + addresses: [192.168.20.1/16] + vlans: + #implicitly handled by OVS because of its link + br-%(ec)s.100: + id: 100 + link: br-%(ec)s''' % {'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, + self.state_dhcp4('br-eth42'), + 'br-data', + 'br-eth42.100']) + # Basic verification that the interfaces/ports are set up in OVS + out = subprocess.check_output(['ovs-vsctl', 'show']) + self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) + self.assertIn(b''' Port %(ec)b + Interface %(ec)b''' % {b'ec': self.dev_e_client.encode()}, out) + self.assertIn(b''' Port br-%(ec)b.100 + tag: 100 + Interface br-%(ec)b.100 + type: internal''' % {b'ec': self.dev_e_client.encode()}, out) + self.assertIn(b' Bridge br-data', out) + self.assert_iface('br-%s' % self.dev_e_client, + ['inet 192.168.5.[0-9]+/16', 'mtu 9000']) # from DHCP + self.assert_iface('br-data', ['inet 192.168.20.1/16']) + self.assert_iface(self.dev_e_client, ['mtu 9000', 'master ovs-system']) + self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', 'br-to-vlan', + 'br-%s.100' % self.dev_e_client])) + self.assertIn(b'br-%b' % self.dev_e_client.encode(), subprocess.check_output( + ['ovs-vsctl', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) + self.assertIn(b'br-%b' % self.dev_e_client.encode(), out) + + def test_bridge_base(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) + self.addCleanup(subprocess.call, ['ovs-vsctl', 'del-ssl']) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: + %(ec)s: {} + %(e2c)s: {} + openvswitch: + ssl: + ca-cert: /some/ca-cert.pem + certificate: /another/certificate.pem + private-key: /private/key.pem + bridges: + ovsbr: + addresses: [192.170.1.1/24] + interfaces: [%(ec)s, %(e2c)s] + openvswitch: + fail-mode: secure + controller: + addresses: [tcp:127.0.0.1, "pssl:1337:[::1]", unix:/some/socket] +''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) + # Basic verification that the interfaces/ports are in OVS + out = subprocess.check_output(['ovs-vsctl', 'show']) + self.assertIn(b' Bridge ovsbr', out) + self.assertIn(b' Controller "tcp:127.0.0.1"', out) + self.assertIn(b' Controller "pssl:1337:[::1]"', out) + self.assertIn(b' Controller "unix:/some/socket"', out) + self.assertIn(b' fail_mode: secure', out) + self.assertIn(b' Port %(ec)b\n Interface %(ec)b' % {b'ec': self.dev_e_client.encode()}, out) + self.assertIn(b' Port %(e2c)b\n Interface %(e2c)b' % {b'e2c': self.dev_e2_client.encode()}, out) + # Verify the bridge was tagged 'netplan:true' correctly + out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', + 'list', 'Bridge', 'ovsbr']) + self.assertIn(b'netplan=true', out) + self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) + + def test_bond_base(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'mybond']) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: + %(ec)s: {} + %(e2c)s: {} + bonds: + mybond: + interfaces: [%(ec)s, %(e2c)s] + parameters: + mode: balance-slb + openvswitch: + lacp: off + bridges: + ovsbr: + addresses: [192.170.1.1/24] + interfaces: [mybond]''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) + # Basic verification that the interfaces/ports are in OVS + out = subprocess.check_output(['ovs-vsctl', 'show']) + self.assertIn(b' Bridge ovsbr', out) + self.assertIn(b' Port mybond', out) + self.assertIn(b' Interface %b' % self.dev_e_client.encode(), out) + self.assertIn(b' Interface %b' % self.dev_e2_client.encode(), out) + # Verify the bond was tagged 'netplan:true' correctly + out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Port']) + self.assertIn(b'mybond,netplan=true', out) + # Verify bond params + out = subprocess.check_output(['ovs-appctl', 'bond/show', 'mybond']) + self.assertIn(b'---- mybond ----', out) + self.assertIn(b'bond_mode: balance-slb', out) + self.assertIn(b'lacp_status: off', out) + self.assertRegex(out, br'(slave|member) %b: enabled' % self.dev_e_client.encode()) + self.assertRegex(out, br'(slave|member) %b: enabled' % self.dev_e2_client.encode()) + self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) + + def test_bridge_patch_ports(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) + with open(self.config, 'w') as f: + f.write('''network: + openvswitch: + ports: + - [patch0-1, patch1-0] + bridges: + br0: + addresses: [192.168.1.1/24] + interfaces: [patch0-1] + br1: + addresses: [192.168.2.1/24] + interfaces: [patch1-0]''') + self.generate_and_settle(['br0', 'br1']) + # Basic verification that the interfaces/ports are set up in OVS + out = subprocess.check_output(['ovs-vsctl', 'show']) + self.assertIn(b' Bridge br0', out) + self.assertIn(b''' Port patch0-1 + Interface patch0-1 + type: patch + options: {peer=patch1-0}''', out) + self.assertIn(b' Bridge br1', out) + self.assertIn(b''' Port patch1-0 + Interface patch1-0 + type: patch + options: {peer=patch0-1}''', out) + self.assert_iface('br0', ['inet 192.168.1.1/24']) + self.assert_iface('br1', ['inet 192.168.2.1/24']) + + def test_bridge_non_ovs_bond(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs-br']) + self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'non-ovs-bond']) + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + ethernets: + %(ec)s: {} + %(e2c)s: {} + bonds: + non-ovs-bond: + interfaces: [%(ec)s, %(e2c)s] + bridges: + ovs-br: + interfaces: [non-ovs-bond] + openvswitch: {}''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovs-br', 'non-ovs-bond']) + # Basic verification that the interfaces/ports are set up in OVS + out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) + self.assertIn(' Bridge ovs-br', out) + self.assertIn(''' Port non-ovs-bond + Interface non-ovs-bond''', out) + self.assertIn(''' Port ovs-br + Interface ovs-br + type: internal''', out) + self.assert_iface('non-ovs-bond', ['master ovs-system']) + self.assert_iface(self.dev_e_client, ['master non-ovs-bond']) + self.assert_iface(self.dev_e2_client, ['master non-ovs-bond']) + + def test_vlan_maas(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', '%s.21' % self.dev_e_client], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + bridges: + ovs0: + addresses: [10.5.48.11/20] + interfaces: [%(ec)s.21] + macaddress: 00:1f:16:15:78:6f + mtu: 1500 + nameservers: + addresses: [10.5.32.99] + search: [maas] + openvswitch: {} + parameters: + forward-delay: 15 + stp: false + ethernets: + %(ec)s: + addresses: [10.5.32.26/20] + gateway4: 10.5.32.1 + mtu: 1500 + nameservers: + addresses: [10.5.32.99] + search: [maas] + vlans: + %(ec)s.21: + id: 21 + link: %(ec)s + mtu: 1500''' % {'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, 'ovs0', 'eth42.21']) + # Basic verification that the interfaces/ports are set up in OVS + out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) + self.assertIn(' Bridge ovs0', out) + self.assertIn(''' Port %(ec)s.21 + Interface %(ec)s.21''' % {'ec': self.dev_e_client}, out) + self.assertIn(''' Port ovs0 + Interface ovs0 + type: internal''', out) + self.assert_iface('ovs0', ['inet 10.5.48.11/20']) + self.assert_iface_up(self.dev_e_client, ['inet 10.5.32.26/20']) + self.assert_iface_up('%s.21' % self.dev_e_client, ['%(ec)s.21@%(ec)s' % {'ec': self.dev_e_client}]) + + def test_missing_ovs_tools(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['mv', '/usr/bin/ovs-vsctl.bak', '/usr/bin/ovs-vsctl']) + subprocess.check_call(['mv', '/usr/bin/ovs-vsctl', '/usr/bin/ovs-vsctl.bak']) + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + bridges: + ovs0: + interfaces: [%(ec)s] + openvswitch: {} + ethernets: + %(ec)s: {}''' % {'ec': self.dev_e_client}) + p = subprocess.Popen(['netplan', 'apply'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True) + (out, err) = p.communicate() + self.assertIn('ovs0: The \'ovs-vsctl\' tool is required to setup OpenVSwitch interfaces.', err) + self.assertNotEqual(p.returncode, 0) + + def test_settings_tag_cleanup(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + openvswitch: + protocols: [OpenFlow13, OpenFlow14, OpenFlow15] + ports: + - [patch0-1, patch1-0] + ssl: + ca-cert: /some/ca-cert.pem + certificate: /another/cert.pem + private-key: /private/key.pem + external-ids: + somekey: 55:44:33:22:11:00 + other-config: + key: value + ethernets: + %(ec)s: + addresses: [10.5.32.26/20] + openvswitch: + external-ids: + iface-id: mylocaliface + other-config: + disable-in-band: false + %(e2c)s: {} + bonds: + bond0: + interfaces: [patch1-0, %(e2c)s] + openvswitch: + lacp: passive + parameters: + mode: balance-tcp + bridges: + ovs0: + addresses: [10.5.48.11/20] + interfaces: [patch0-1, %(ec)s, bond0] + openvswitch: + protocols: [OpenFlow10, OpenFlow11, OpenFlow12] + controller: + addresses: [unix:/var/run/openvswitch/ovs0.mgmt] + connection-mode: out-of-band + fail-mode: secure + mcast-snooping: true + external-ids: + iface-id: myhostname + other-config: + disable-in-band: true + hwaddr: aa:bb:cc:dd:ee:ff + ovs1: + openvswitch: + # Add ovs1 as rstp cannot be used if bridge contains a bond interface + rstp: true + +''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovs0', 'ovs1']) + before = self._collect_ovs_settings('ovs0') + subprocess.check_call(['netplan', 'apply', '--only-ovs-cleanup']) + after = self._collect_ovs_settings('ovs0') + + # Verify interfaces + for data in (before['show'], after['show']): + self.assertIn(b'Bridge ovs0', data) + self.assertIn(b'Port ovs0', data) + self.assertIn(b'Interface ovs0', data) + self.assertIn(b'Port patch0-1', data) + self.assertIn(b'Interface patch0-1', data) + self.assertIn(b'Port eth42', data) + self.assertIn(b'Interface eth42', data) + self.assertIn(b'Bridge ovs1', data) + self.assertIn(b'Port ovs1', data) + self.assertIn(b'Interface ovs1', data) + self.assertIn(b'Port bond0', data) + self.assertIn(b'Interface eth42', data) + self.assertIn(b'Interface patch1-0', data) + # Verify all settings tags have been removed + for tbl in ('Open_vSwitch', 'Controller', 'Bridge', 'Port', 'Interface'): + self.assertNotIn(b'netplan/', after['external-ids-%s' % tbl]) + # Verify SSL + for s in (b'Private key: /private/key.pem', b'Certificate: /another/cert.pem', b'CA Certificate: /some/ca-cert.pem'): + self.assertIn(s, before['ssl']) + self.assertNotIn(s, after['ssl']) + # Verify Bond + self.assertIn(b'bond0,balance-tcp\n', before['bond_mode-Bond']) + self.assertIn(b'bond0,\n', after['bond_mode-Bond']) + self.assertIn(b'bond0,passive\n', before['lacp-Bond']) + self.assertIn(b'bond0,\n', after['lacp-Bond']) + # Verify Bridge + self.assertIn(b'secure', before['set-fail-mode-Bridge']) + self.assertNotIn(b'secure', after['set-fail-mode-Bridge']) + self.assertIn(b'ovs0,true\n', before['mcast_snooping_enable-Bridge']) + self.assertIn(b'ovs0,false\n', after['mcast_snooping_enable-Bridge']) + self.assertIn(b'ovs1,true\n', before['rstp_enable-Bridge']) + self.assertIn(b'ovs1,false\n', after['rstp_enable-Bridge']) + self.assertIn(b'ovs0,OpenFlow10 OpenFlow11 OpenFlow12\n', before['protocols-Bridge']) + self.assertIn(b'ovs0,\n', after['protocols-Bridge']) + # Verify global protocols + self.assertIn(b'ovs1,OpenFlow13 OpenFlow14 OpenFlow15\n', before['protocols-Bridge']) + self.assertIn(b'ovs1,\n', after['protocols-Bridge']) + # Verify Controller + self.assertIn(b'Controller "unix:/var/run/openvswitch/ovs0.mgmt"', before['show']) + self.assertNotIn(b'Controller', after['show']) + self.assertIn(b'unix:/var/run/openvswitch/ovs0.mgmt', before['set-controller-Bridge']) + self.assertIn(b',out-of-band', before['connection_mode-Controller']) + self.assertEqual(b'', after['set-controller-Bridge']) + self.assertEqual(b'', after['connection_mode-Controller']) + # Verify other-config + self.assertIn(b'key=value', before['other-config-Open_vSwitch']) + self.assertNotIn(b'key=value', after['other-config-Open_vSwitch']) + self.assertIn(b'hwaddr=aa:bb:cc:dd:ee:ff', before['other-config-Bridge']) + self.assertNotIn(b'hwaddr=aa:bb:cc:dd:ee:ff', after['other-config-Bridge']) + self.assertIn(b'ovs0,disable-in-band=true', before['other-config-Bridge']) + self.assertIn(b'ovs0,\n', after['other-config-Bridge']) + self.assertIn(b'eth42,disable-in-band=false\n', before['other-config-Interface']) + self.assertIn(b'eth42,\n', after['other-config-Interface']) + # Verify external-ids + self.assertIn(b'somekey=55:44:33:22:11:00', before['external-ids-Open_vSwitch']) + self.assertNotIn(b'somekey=55:44:33:22:11:00', after['external-ids-Open_vSwitch']) + self.assertIn(b'iface-id=myhostname', before['external-ids-Bridge']) + self.assertNotIn(b'iface-id=myhostname', after['external-ids-Bridge']) + self.assertIn(b'iface-id=mylocaliface', before['external-ids-Interface']) + self.assertNotIn(b'iface-id=mylocaliface', after['external-ids-Interface']) + for tbl in ('Bridge', 'Port'): + # The netplan=true tag shall be kept unitl the interface is deleted + self.assertIn(b'netplan=true', before['external-ids-%s' % tbl]) + self.assertIn(b'netplan=true', after['external-ids-%s' % tbl]) + + @unittest.skip("For debugging only") + def test_zzz_ovs_debugging(self): # Runs as the last test, to collect all logs + """Display OVS logs of the previous tests""" + out = subprocess.check_output(['cat', '/var/log/openvswitch/ovs-vswitchd.log'], universal_newlines=True) + print(out) + out = subprocess.check_output(['ovsdb-tool', 'show-log'], universal_newlines=True) + print(out) + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestOVS(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/regressions.py b/tests/integration/regressions.py new file mode 100644 index 0000000..25e9a2d --- /dev/null +++ b/tests/integration/regressions.py @@ -0,0 +1,90 @@ +#!/usr/bin/python3 +# +# Regression tests to catch previously-fixed issues. +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_empty_yaml_lp1795343(self): + with open(self.config, 'w') as f: + f.write('''''') + self.generate_and_settle([]) + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_lp1802322_bond_mac_rename(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn1: + match: {name: %(ec)s} + dhcp4: no + ethbn2: + match: {name: %(e2c)s} + dhcp4: no + bonds: + mybond: + interfaces: [ethbn1, ethbn2] + macaddress: 00:0a:f7:72:a7:28 + mtu: 9000 + addresses: [ 192.168.5.9/24 ] + gateway4: 192.168.5.1 + parameters: + down-delay: 0 + lacp-rate: fast + mii-monitor-interval: 100 + mode: 802.3ad + transmit-hash-policy: layer3+4 + up-delay: 0 + ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'mybond']) + self.assert_iface_up(self.dev_e_client, + ['master mybond', '00:0a:f7:72:a7:28'], + ['inet ']) + self.assert_iface_up(self.dev_e2_client, + ['master mybond', '00:0a:f7:72:a7:28'], + ['inet ']) + self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + self.assertIn(self.dev_e_client, f.read().strip()) + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/routing.py b/tests/integration/routing.py new file mode 100644 index 0000000..d2b3075 --- /dev/null +++ b/tests/integration/routing.py @@ -0,0 +1,339 @@ +#!/usr/bin/python3 +# +# Integration tests for routing functions +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + # Supposed to fail if tested against NetworkManager < 1.12/1.18 + # The on-link option was introduced as of NM 1.12+ (for IPv4) + # The on-link option was introduced as of NM 1.18+ (for IPv6) + def test_route_on_link(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + addresses: ["9876:BBBB::11/70"] + routes: + - to: 2001:f00f:f00f::1/64 + via: 9876:BBBB::5 + on-link: true''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet6 9876:bbbb::11/70']) + out = subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client], + universal_newlines=True) + # NM routes have a (default) 'metric' in between 'proto static' and 'onlink' + self.assertRegex(out, r'2001:f00f:f00f::/64 via 9876:bbbb::5 proto static[^\n]* onlink') + + # Supposed to fail if tested against NetworkManager < 1.8 + # The from option was introduced as of NM 1.8+ + def test_route_from(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + from: 192.168.14.2''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 192.168.14.2']) + out = subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client], + universal_newlines=True) + self.assertIn('10.10.10.0/24 via 192.168.14.20 proto static src 192.168.14.2', out) + + # Supposed to fail if tested against NetworkManager < 1.10 + # The table option was introduced as of NM 1.10+ + def test_route_table(self): + self.setup_eth(None) + table_id = '255' # This is the 'local' FIB of /etc/iproute2/rt_tables + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + dhcp4: no + addresses: [ "10.20.10.2/24" ] + gateway4: 10.20.10.1 + routes: + - to: 10.0.0.0/8 + via: 11.0.0.1 + table: %(tid)s + on-link: true''' % {'r': self.backend, 'ec': self.dev_e_client, 'tid': table_id}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet ']) + out = subprocess.check_output(['ip', 'route', 'show', 'table', table_id, 'dev', + self.dev_e_client], universal_newlines=True) + # NM routes have a (default) 'metric' in between 'proto static' and 'onlink' + self.assertRegex(out, r'10\.0\.0\.0/8 via 11\.0\.0\.1 proto static[^\n]* onlink') + + @unittest.skip("fails due to networkd bug setting routes with dhcp") + def test_routes_v4_with_dhcp(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + dhcp4: yes + routes: + - to: 10.10.10.0/24 + via: 192.168.5.254 + metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.state_dhcp4(self.dev_e_client)]) + self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) # from DHCP + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'10.10.10.0/24 via 192.168.5.254', # from static route + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'metric 99', # check metric from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + + def test_routes_v4(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + gateway4: 192.168.5.1 + routes: + - to: 10.10.10.0/24 + via: 192.168.5.254 + metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) # from DHCP + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'10.10.10.0/24 via 192.168.5.254', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'metric 99', # check metric from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + + def test_routes_v6(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: ["9876:BBBB::11/70"] + gateway6: "9876:BBBB::1" + routes: + - to: 2001:f00f:f00f::1/64 + via: 9876:BBBB::5 + metric: 799''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet6 9876:bbbb::11/70']) + self.assertNotIn(b'default', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'via 9876:bbbb::1', + subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) + self.assertIn(b'2001:f00f:f00f::/64 via 9876:bbbb::5', + subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'metric 799', + subprocess.check_output(['ip', '-6', 'route', 'show', '2001:f00f:f00f::/64'])) + + def test_routes_default(self): + self.setup_eth(None, False) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + - "9876:BBBB::11/70" + routes: + - to: default + via: 192.168.5.1 + - to: default + via: "9876:BBBB::1" + - to: 10.10.10.0/24 + via: 192.168.5.254 + metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.99/24', 'inet6 9876:bbbb::11/70']) + # import pdb + # pdb.set_trace() + self.assertIn(b'default via 192.168.5.1', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'via 9876:bbbb::1', + subprocess.check_output(['ip', '-6', 'route', 'show', 'default'])) + self.assertIn(b'10.10.10.0/24 via 192.168.5.254', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'metric 99', # check metric from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + + def test_per_route_mtu(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + gateway4: 192.168.5.1 + routes: + - to: 10.10.10.0/24 + via: 192.168.5.254 + mtu: 777''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assertIn(b'mtu 777', # check mtu from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + + def test_per_route_congestion_window(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + gateway4: 192.168.5.1 + routes: + - to: 10.10.10.0/24 + via: 192.168.5.254 + congestion-window: 16''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assertIn(b'initcwnd 16', # check initcwnd from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + + def test_per_route_advertised_receive_window(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + gateway4: 192.168.5.1 + routes: + - to: 10.10.10.0/24 + via: 192.168.5.254 + advertised-receive-window: 16''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assertIn(b'initrwnd 16', # check initrwnd from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_link_route_v4(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + gateway4: 192.168.5.1 + routes: + - to: 10.10.10.0/24 + scope: link + metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) # from DHCP + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'10.10.10.0/24 proto static scope link', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + self.assertIn(b'metric 99', # check metric from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + + @unittest.skip("networkd does not handle non-unicast routes correctly yet (Invalid argument)") + def test_route_type_blackhole(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + addresses: [ "10.20.10.1/24" ] + routes: + - to: 10.10.10.0/24 + via: 10.20.10.100 + type: blackhole''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet ']) + self.assertIn(b'blackhole 10.10.10.0/24', + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client])) + + def test_route_with_policy(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + addresses: [ "10.20.10.1/24" ] + routes: + - to: 40.0.0.0/24 + via: 10.20.10.55 + metric: 50 + - to: 40.0.0.0/24 + via: 10.20.10.88 + table: 99 + metric: 50 + routing-policy: + - from: 10.20.10.0/24 + to: 40.0.0.0/24 + table: 99''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet ']) + self.assertIn(b'to 40.0.0.0/24 lookup 99', + subprocess.check_output(['ip', 'rule', 'show'])) + self.assertIn(b'40.0.0.0/24 via 10.20.10.88', + subprocess.check_output(['ip', 'route', 'show', 'table', '99'])) + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/run.py b/tests/integration/run.py new file mode 100755 index 0000000..deb8e4b --- /dev/null +++ b/tests/integration/run.py @@ -0,0 +1,85 @@ +#!/usr/bin/python3 +# +# Test runner for netplan integration tests. +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import glob +import os +import subprocess +import textwrap +import sys + +tests_dir = os.path.dirname(os.path.abspath(__file__)) + +default_backends = [ 'networkd', 'NetworkManager' ] +fixtures = [ "__init__.py", "base.py", "run.py" ] + +possible_tests = [] +testfiles = glob.glob(os.path.join(tests_dir, "*.py")) +for pyfile in testfiles: + filename = os.path.basename(pyfile) + if filename not in fixtures: + possible_tests.append(filename.split('.')[0]) + +def dedupe(duped_list): + deduped = set() + for item in duped_list: + real_items = item.split(",") + for real_item in real_items: + deduped.add(real_item) + return deduped + +# XXX: omg, this is ugly :) +parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent(""" +Test runner for netplan integration tests + +Available tests: +{} +""".format("\n".join(" - {}".format(x) for x in sorted(possible_tests))))) + +parser.add_argument('--test', action='append', help="List of tests to be run") +parser.add_argument('--backend', action='append', help="List of backends to test (NetworkManager, networkd)") + +args = parser.parse_args() + +requested_tests = set() +backends = set() + +if args.test is not None: + requested_tests = dedupe(args.test) +else: + requested_tests.update(possible_tests) + +if args.backend is not None: + backends = dedupe(args.backend) +else: + backends.update(default_backends) + +os.environ["NETPLAN_TEST_BACKENDS"] = ",".join(backends) + +returncode = 0 +for test in requested_tests: + ret = subprocess.call(['python3', os.path.join(tests_dir, "{}.py".format(test))]) + if returncode == 0 and ret != 0: + returncode = ret + +sys.exit(returncode) diff --git a/tests/integration/scenarios.py b/tests/integration/scenarios.py new file mode 100644 index 0000000..93f8a4a --- /dev/null +++ b/tests/integration/scenarios.py @@ -0,0 +1,123 @@ +#!/usr/bin/python3 +# +# Integration tests for complex networking scenarios +# (ie. mixes of various features, may test real live cases) +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_mix_bridge_on_bond(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + bridges: + br0: + interfaces: [bond0] + addresses: ['192.168.0.2/24'] + bonds: + bond0: + interfaces: [ethb2] + parameters: + mode: balance-rr + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'br0', 'bond0']) + self.assert_iface_up(self.dev_e2_client, ['master bond0'], ['inet ']) + self.assert_iface_up('bond0', ['master br0']) + self.assert_iface('br0', ['inet 192.168.0.2/24']) + with open('/sys/class/net/bond0/bonding/slaves') as f: + result = f.read().strip() + self.assertIn(self.dev_e2_client, result) + + def test_mix_vlan_on_bridge_on_bond(self): + self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + vlans: + vlan1: + link: 'br0' + id: 1 + addresses: [ '10.10.10.1/24' ] + bridges: + br0: + interfaces: ['bond0', 'vlan2'] + parameters: + stp: false + path-cost: + bond0: 1000 + vlan2: 2000 + bonds: + bond0: + interfaces: ['br1'] + parameters: + mode: balance-rr + bridges: + br1: + interfaces: ['ethb2'] + vlans: + vlan2: + link: ethbn + id: 2 + ethernets: + ethbn: + match: {name: %(ec)s} + ethb2: + match: {name: %(e2c)s} +''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'br0', 'br1', 'bond0', 'vlan1', 'vlan2']) + self.assert_iface_up('vlan1', ['vlan1@br0']) + self.assert_iface_up('vlan2', ['vlan2@' + self.dev_e_client, 'master br0']) + self.assert_iface_up(self.dev_e2_client, ['master br1'], ['inet ']) + self.assert_iface_up('bond0', ['master br0']) + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/tunnels.py b/tests/integration/tunnels.py new file mode 100644 index 0000000..c336954 --- /dev/null +++ b/tests/integration/tunnels.py @@ -0,0 +1,221 @@ +#!/usr/bin/python3 +# Tunnel integration tests. NM and networkd are started on the generated +# configuration, using emulated ethernets (veth). +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import time +import unittest + +from base import IntegrationTestsBase, test_backends + +class _CommonTests(): + + def test_tunnel_sit(self): + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'sit-tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + tunnels: + sit-tun0: + mode: sit + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend}) + self.generate_and_settle(['sit-tun0']) + self.assert_iface('sit-tun0', ['sit-tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + def test_tunnel_ipip(self): + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + tunnels: + tun0: + mode: ipip + local: 192.168.5.1 + remote: 99.99.99.99 + ttl: 64 +''' % {'r': self.backend}) + self.generate_and_settle(['tun0']) + self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + def test_tunnel_wireguard(self): + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'wg0'], stderr=subprocess.DEVNULL) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'wg1'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + tunnels: + wg0: #server + mode: wireguard + addresses: [10.10.10.20/24] + gateway4: 10.10.10.21 + key: 4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8= + mark: 42 + port: 51820 + peers: + - keys: + public: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= + shared: 7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= + allowed-ips: [20.20.20.10/24] + wg1: #client + mode: wireguard + addresses: [20.20.20.10/24] + gateway4: 20.20.20.11 + key: KPt9BzQjejRerEv8RMaFlpsD675gNexELOQRXt/AcH0= + peers: + - endpoint: 10.10.10.20:51820 + allowed-ips: [0.0.0.0/0] + keys: + public: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc= + shared: 7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= + keepalive: 21 +''' % {'r': self.backend}) + self.generate_and_settle(['wg0', 'wg1']) + # Wait for handshake/connection between client & server + self.wait_output(['wg', 'show', 'wg0'], 'latest handshake') + self.wait_output(['wg', 'show', 'wg1'], 'latest handshake') + # Verify server + out = subprocess.check_output(['wg', 'show', 'wg0', 'private-key'], universal_newlines=True) + self.assertIn("4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=", out) + out = subprocess.check_output(['wg', 'show', 'wg0', 'preshared-keys'], universal_newlines=True) + self.assertIn("7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8=", out) + out = subprocess.check_output(['wg', 'show', 'wg0'], universal_newlines=True) + self.assertIn("public key: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc=", out) + self.assertIn("listening port: 51820", out) + self.assertIn("fwmark: 0x2a", out) + self.assertIn("peer: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=", out) + self.assertIn("allowed ips: 20.20.20.0/24", out) + self.assertRegex(out, r'latest handshake: (\d+ seconds? ago|Now)') + self.assertRegex(out, r'transfer: \d+.*B received, \d+.*B sent') + self.assert_iface('wg0', ['inet 10.10.10.20/24']) + # Verify client + out = subprocess.check_output(['wg', 'show', 'wg1', 'private-key'], universal_newlines=True) + self.assertIn("KPt9BzQjejRerEv8RMaFlpsD675gNexELOQRXt/AcH0=", out) + out = subprocess.check_output(['wg', 'show', 'wg1', 'preshared-keys'], universal_newlines=True) + self.assertIn("7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8=", out) + out = subprocess.check_output(['wg', 'show', 'wg1'], universal_newlines=True) + self.assertIn("public key: M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=", out) + self.assertIn("peer: rlbInAj0qV69CysWPQY7KEBnKxpYCpaWqOs/dLevdWc=", out) + self.assertIn("endpoint: 10.10.10.20:51820", out) + self.assertIn("allowed ips: 0.0.0.0/0", out) + self.assertIn("persistent keepalive: every 21 seconds", out) + self.assertRegex(out, r'latest handshake: (\d+ seconds? ago|Now)') + self.assertRegex(out, r'transfer: \d+.*B received, \d+.*B sent') + self.assert_iface('wg1', ['inet 20.20.20.10/24']) + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + def test_tunnel_gre(self): + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + tunnels: + tun0: + mode: gre + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend}) + self.generate_and_settle(['tun0']) + self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + def test_tunnel_gre6(self): + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + tunnels: + tun0: + mode: ip6gre + local: fe80::1 + remote: 2001:dead:beef::2 +''' % {'r': self.backend}) + self.generate_and_settle(['tun0']) + self.assert_iface('tun0', ['tun0@NONE', 'link.* fe80::1 brd 2001:dead:beef::2']) + + def test_tunnel_vti(self): + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + tunnels: + tun0: + mode: vti + keys: 1234 + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend}) + self.generate_and_settle(['tun0']) + self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + def test_tunnel_vti6(self): + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + tunnels: + tun0: + mode: vti6 + keys: 1234 + local: fe80::1 + remote: 2001:dead:beef::2 +''' % {'r': self.backend}) + self.generate_and_settle(['tun0']) + self.assert_iface('tun0', ['tun0@NONE', 'link.* fe80::1 brd 2001:dead:beef::2']) + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + def test_tunnel_gre(self): + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'tun0'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + version: 2 + tunnels: + tun0: + mode: gre + keys: 1234 + local: 192.168.5.1 + remote: 99.99.99.99 +''' % {'r': self.backend}) + self.generate_and_settle(['tun0']) + self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/vlans.py b/tests/integration/vlans.py new file mode 100644 index 0000000..e68f605 --- /dev/null +++ b/tests/integration/vlans.py @@ -0,0 +1,103 @@ +#!/usr/bin/python3 +# +# Integration tests for VLAN virtual devices +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsBase, test_backends + + +class _CommonTests(): + + def test_vlan(self): + # we create two VLANs on e2c, and run dnsmasq on ID 2002 to test DHCP via VLAN + self.setup_eth(None, start_dnsmasq=False) + self.start_dnsmasq(None, self.dev_e2_ap) + subprocess.check_call(['ip', 'link', 'add', 'link', self.dev_e2_ap, + 'name', 'nptestsrv', 'type', 'vlan', 'id', '2002']) + subprocess.check_call(['ip', 'a', 'add', '192.168.5.1/24', 'dev', 'nptestsrv']) + subprocess.check_call(['ip', 'link', 'set', 'nptestsrv', 'up']) + self.start_dnsmasq(None, 'nptestsrv') + with open(self.config, 'w') as f: + f.write('''network: + version: 2 + renderer: %(r)s + ethernets: + myether: + match: {name: %(e2c)s} + dhcp4: yes + vlans: + nptestone: + id: 1001 + link: myether + addresses: [10.9.8.7/24] + nptesttwo: + id: 2002 + link: myether + dhcp4: true + ''' % {'r': self.backend, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.state_dhcp4(self.dev_e2_client), + 'nptestone', + self.state_dhcp4('nptesttwo')]) + self.assert_iface_up('nptestone', ['nptestone@' + self.dev_e2_client, 'inet 10.9.8.7/24']) + self.assert_iface_up('nptesttwo', ['nptesttwo@' + self.dev_e2_client, 'inet 192.168.5']) + self.assertNotIn(b'default', + subprocess.check_output(['ip', 'route', 'show', 'dev', 'nptestone'])) + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', 'nptesttwo'])) + + def test_vlan_mac_address(self): + self.setup_eth(None) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'myvlan'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + ethbn: + match: {name: %(ec)s} + vlans: + myvlan: + id: 101 + link: ethbn + macaddress: aa:bb:cc:dd:ee:22 + ''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, 'myvlan']) + self.assert_iface_up('myvlan', ['myvlan@' + self.dev_e_client]) + with open('/sys/class/net/myvlan/address') as f: + self.assertEqual(f.read().strip(), 'aa:bb:cc:dd:ee:22') + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsBase, _CommonTests): + backend = 'networkd' + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsBase, _CommonTests): + backend = 'NetworkManager' + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/integration/wifi.py b/tests/integration/wifi.py new file mode 100644 index 0000000..d09b5cf --- /dev/null +++ b/tests/integration/wifi.py @@ -0,0 +1,158 @@ +#!/usr/bin/python3 +# +# Integration tests for wireless devices +# +# These need to be run in a VM and do change the system +# configuration. +# +# Copyright (C) 2018-2021 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import sys +import subprocess +import unittest + +from base import IntegrationTestsWifi, test_backends + + +class _CommonTests(): + + @unittest.skip("Unsupported matching by driver / wifi matching makes this untestable for now") + def test_mapping_for_driver(self): + self.setup_ap('hw_mode=b\nchannel=1\nssid=fake net', None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + wifis: + wifi_ifs: + match: + driver: mac80211_hwsim + dhcp4: yes + access-points: + "fake net": {} + decoy: {}''' % {'r': self.backend}) + self.generate_and_settle([self.state_dhcp4(self.dev_w_client)]) + p = subprocess.Popen(['netplan', 'generate', '--mapping', 'mac80211_hwsim'], + stdout=subprocess.PIPE) + out = p.communicate()[0] + self.assertEquals(p.returncode, 1) + self.assertIn(b'mac80211_hwsim', out) + + def test_wifi_ipv4_open(self): + self.setup_ap('hw_mode=b\nchannel=1\nssid=fake net', None) + + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + wifis: + %(wc)s: + dhcp4: yes + access-points: + "fake net": {} + decoy: {}''' % {'r': self.backend, 'wc': self.dev_w_client}) + self.generate_and_settle([self.state_dhcp4(self.dev_w_client)]) + self.assert_iface_up(self.dev_w_client, ['inet 192.168.5.[0-9]+/24']) + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_w_client])) + if self.backend == 'NetworkManager': + out = subprocess.check_output(['nmcli', 'dev', 'show', self.dev_w_client], + universal_newlines=True) + self.assertRegex(out, 'GENERAL.CONNECTION.*netplan-%s-fake net' % self.dev_w_client) + self.assertRegex(out, 'IP4.DNS.*192.168.5.1') + else: + out = subprocess.check_output(['networkctl', 'status', self.dev_w_client], + universal_newlines=True) + self.assertRegex(out, 'DNS.*192.168.5.1') + + def test_wifi_ipv4_wpa2(self): + self.setup_ap('''hw_mode=g +channel=1 +ssid=fake net +wpa=1 +wpa_key_mgmt=WPA-PSK +wpa_pairwise=TKIP +wpa_passphrase=12345678 +''', None) + + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + wifis: + %(wc)s: + dhcp4: yes + access-points: + "fake net": + password: 12345678 + decoy: {}''' % {'r': self.backend, 'wc': self.dev_w_client}) + self.generate_and_settle([self.state_dhcp4(self.dev_w_client)]) + self.assert_iface_up(self.dev_w_client, ['inet 192.168.5.[0-9]+/24']) + self.assertIn(b'default via 192.168.5.1', # from DHCP + subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_w_client])) + if self.backend == 'NetworkManager': + out = subprocess.check_output(['nmcli', 'dev', 'show', self.dev_w_client], + universal_newlines=True) + self.assertRegex(out, 'GENERAL.CONNECTION.*netplan-%s-fake net' % self.dev_w_client) + self.assertRegex(out, 'IP4.DNS.*192.168.5.1') + else: + out = subprocess.check_output(['networkctl', 'status', self.dev_w_client], + universal_newlines=True) + self.assertRegex(out, 'DNS.*192.168.5.1') + + +@unittest.skipIf("networkd" not in test_backends, + "skipping as networkd backend tests are disabled") +class TestNetworkd(IntegrationTestsWifi, _CommonTests): + backend = 'networkd' + + +@unittest.skipIf("NetworkManager" not in test_backends, + "skipping as NetworkManager backend tests are disabled") +class TestNetworkManager(IntegrationTestsWifi, _CommonTests): + backend = 'NetworkManager' + + def test_wifi_ap_open(self): + # we use dev_w_client and dev_w_ap in switched roles here, to keep the + # existing device blacklisting in NM; i. e. dev_w_client is the + # NM-managed AP, and dev_w_ap the manually managed client + with open(self.config, 'w') as f: + f.write('''network: + wifis: + renderer: NetworkManager + %(wc)s: + dhcp4: yes + access-points: + "fake net": + mode: ap''' % {'wc': self.dev_w_client}) + self.generate_and_settle([self.state(self.dev_w_client, 'inet 10.')]) + out = subprocess.check_output(['iw', 'dev', self.dev_w_client, 'info'], + universal_newlines=True) + self.assertIn('type AP', out) + self.assertIn('ssid fake net', out) + + # connect the other end + subprocess.check_call(['ip', 'link', 'set', self.dev_w_ap, 'up']) + subprocess.check_call(['iw', 'dev', self.dev_w_ap, 'connect', 'fake net']) + out = subprocess.check_output(['dhclient', '-1', '-v', self.dev_w_ap], + stderr=subprocess.STDOUT, universal_newlines=True) + self.assertIn('DHCPACK', out) + out = subprocess.check_output(['iw', 'dev', self.dev_w_ap, 'info'], + universal_newlines=True) + self.assertIn('type managed', out) + self.assertIn('ssid fake net', out) + self.assert_iface_up(self.dev_w_ap, ['inet 10.']) + + +unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff --git a/tests/parser/__init__.py b/tests/parser/__init__.py new file mode 100644 index 0000000..47aeeb5 --- /dev/null +++ b/tests/parser/__init__.py @@ -0,0 +1,17 @@ +# +# __init__ for parser tests. +# +# Copyright (C) 2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . diff --git a/tests/parser/base.py b/tests/parser/base.py new file mode 100644 index 0000000..0ba40f6 --- /dev/null +++ b/tests/parser/base.py @@ -0,0 +1,169 @@ +# +# Blackbox tests of netplan's keyfile parser that verify that the generated +# YAML files look as expected. These are run during "make check" and +# don't touch the system configuration at all. +# +# Copyright (C) 2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from configparser import ConfigParser +import os +import sys +import shutil +import tempfile +import unittest +import ctypes +import ctypes.util +import contextlib +import subprocess + +exe_generate = os.path.join(os.path.dirname(os.path.dirname( + os.path.dirname(os.path.abspath(__file__)))), 'generate') + +# make sure we point to libnetplan properly. +os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) + +# make sure we fail on criticals +os.environ['G_DEBUG'] = 'fatal-criticals' + +lib = ctypes.CDLL(ctypes.util.find_library('netplan')) + + +# A contextmanager to catch the output on a low level so that it catches output +# from a subprocess or C library call, in addition to normal python output +@contextlib.contextmanager +def capture_stderr(): + stderr_fd = 2 # 2 = stderr + with tempfile.NamedTemporaryFile(mode='w+b') as tmp: + stderr_copy = os.dup(stderr_fd) + try: + sys.stderr.flush() + os.dup2(tmp.fileno(), stderr_fd) + yield tmp + finally: + sys.stderr.flush() + os.dup2(stderr_copy, stderr_fd) + os.close(stderr_copy) + + +class TestKeyfileBase(unittest.TestCase): + + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + self.confdir = os.path.join(self.workdir.name, 'etc', 'netplan') + self.maxDiff = None + os.makedirs(self.confdir) + + def tearDown(self): + shutil.rmtree(self.workdir.name) + super().tearDown() + + def generate_from_keyfile(self, keyfile, netdef_id=None, expect_fail=False, filename=None): + '''Call libnetplan with given keyfile string as configuration''' + # Autodetect default 'NM-' netdef-id + ssid = '' + if not netdef_id: + found_values = 0 + uuid = 'UNKNOWN_UUID' + for line in keyfile.splitlines(): + if line.startswith('uuid='): + uuid = line.split('=')[1] + found_values += 1 + elif line.startswith('ssid='): + ssid += '-' + line.split('=')[1] + found_values += 1 + if found_values >= 2: + break + netdef_id = 'NM-' + uuid + if not filename: + filename = 'netplan-{}{}.nmconnection'.format(netdef_id, ssid) + f = os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/{}'.format(filename)) + os.makedirs(os.path.dirname(f)) + with open(f, 'w') as file: + file.write(keyfile) + + with capture_stderr() as outf: + if expect_fail: + self.assertFalse(lib.netplan_parse_keyfile(f.encode(), None)) + else: + self.assertTrue(lib.netplan_parse_keyfile(f.encode(), None)) + lib._write_netplan_conf(netdef_id.encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assert_nm_regenerate({filename: keyfile}) # check re-generated keyfile + with open(outf.name, 'r') as f: + output = f.read().strip() # output from stderr (fd=2) on C/library level + return output + + def assert_netplan(self, file_contents_map): + for uuid in file_contents_map.keys(): + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(uuid)))) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(uuid)), 'r') as f: + self.assertEqual(f.read(), file_contents_map[uuid]) + + def normalize_keyfile(self, file_contents): + parser = ConfigParser() + parser.read_string(file_contents) + sections = parser.sections() + res = [] + # Sort sections and keys + sections.sort() + for s in sections: + items = parser.items(s) + if s == 'ipv6' and len(items) == 1 and items[0] == ('method', 'ignore'): + continue + + line = '\n[' + s + ']' + res.append(line) + items.sort(key=lambda tup: tup[0]) + for k, v in items: + # Normalize lines + if k == 'addr-gen-mode': + v = v.replace('1', 'stable-privacy').replace('0', 'eui64') + elif k == 'dns-search' and v != '': + # XXX: netplan is loosing information here about which search domain + # belongs to the [ipv4] or [ipv6] sections + v = '*** REDACTED (in base.py) ***' + # handle NM defaults + elif k == 'dns-search' and v == '': + continue + elif k == 'wake-on-lan' and v == '1': + continue + elif k == 'stp' and v == 'true': + continue + + line = (k + '=' + v).strip(';') + res.append(line) + return '\n'.join(res).strip()+'\n' + + def assert_nm_regenerate(self, file_contents_map): + argv = [exe_generate, '--root-dir', self.workdir.name] + p = subprocess.Popen(argv, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, universal_newlines=True) + (out, err) = p.communicate() + self.assertEqual(out, '') + con_dir = os.path.join(self.workdir.name, 'run', 'NetworkManager', 'system-connections') + if file_contents_map: + self.assertEqual(set(os.listdir(con_dir)), + set([n for n in file_contents_map])) + for fname, contents in file_contents_map.items(): + with open(os.path.join(con_dir, fname)) as f: + generated_keyfile = self.normalize_keyfile(f.read()) + normalized_contents = self.normalize_keyfile(contents) + self.assertEqual(generated_keyfile, normalized_contents, + 'Re-generated keyfile does not match') + else: # pragma: nocover (only needed for test debugging) + if os.path.exists(con_dir): + self.assertEqual(os.listdir(con_dir), []) + return err diff --git a/tests/parser/test_keyfile.py b/tests/parser/test_keyfile.py new file mode 100644 index 0000000..692dba4 --- /dev/null +++ b/tests/parser/test_keyfile.py @@ -0,0 +1,1018 @@ +#!/usr/bin/python3 +# Blackbox tests of NetworkManager keyfile parser. These are run during +# "make check" and don't touch the system configuration at all. +# +# Copyright (C) 2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import ctypes +import ctypes.util + +from .base import TestKeyfileBase + +rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +exe_cli = os.path.join(rootdir, 'src', 'netplan.script') +# Make sure we can import our development netplan. +os.environ.update({'PYTHONPATH': '.'}) + +lib = ctypes.CDLL(ctypes.util.find_library('netplan')) +lib.netplan_get_id_from_nm_filename.restype = ctypes.c_char_p +UUID = 'ff9d6ebc-226d-4f82-a485-b7ff83b9607f' + + +class TestNetworkManagerKeyfileParser(TestKeyfileBase): + '''Test NM keyfile parser as used by NetworkManager's YAML backend''' + + def test_keyfile_missing_uuid(self): + err = self.generate_from_keyfile('[connection]\ntype=ethernets', expect_fail=True) + self.assertIn('netplan: Keyfile: cannot find connection.uuid', err) + + def test_keyfile_missing_type(self): + err = self.generate_from_keyfile('[connection]\nuuid=87749f1d-334f-40b2-98d4-55db58965f5f', expect_fail=True) + self.assertIn('netplan: Keyfile: cannot find connection.type', err) + + def test_keyfile_gsm(self): + self.generate_from_keyfile('''[connection] +id=T-Mobile Funkadelic 2 +uuid={} +type=gsm + +[gsm] +apn=internet2.voicestream.com +device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 +home-only=true +network-id=254098 +password=parliament2 +pin=123456 +sim-id=89148000000060671234 +sim-operator-id=310260 +username=george.clinton.again +mtu=1042 + +[ipv4] +dns-search= +method=auto + +[ipv6] +dns-search= +method=auto +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + modems: + NM-{}: + renderer: NetworkManager + match: {{}} + dhcp4: true + dhcp6: true + mtu: 1042 + apn: "internet2.voicestream.com" + device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" + network-id: "254098" + pin: "123456" + sim-id: "89148000000060671234" + sim-operator-id: "310260" + username: "george.clinton.again" + password: "parliament2" + networkmanager: + uuid: "{}" + name: "T-Mobile Funkadelic 2" + passthrough: + gsm.home-only: "true" +'''.format(UUID, UUID)}) + + def test_keyfile_cdma(self): + self.generate_from_keyfile('''[connection] +id=T-Mobile Funkadelic 2 +uuid={} +type=cdma + +[cdma] +number=0123456 +username=testuser +password=testpass +mtu=1042 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + modems: + NM-{}: + renderer: NetworkManager + match: {{}} + dhcp4: true + mtu: 1042 + username: "testuser" + password: "testpass" + number: "0123456" + networkmanager: + uuid: "{}" + name: "T-Mobile Funkadelic 2" +'''.format(UUID, UUID)}) + + def test_keyfile_gsm_via_bluetooth(self): + self.generate_from_keyfile('''[connection] +id=T-Mobile Funkadelic 2 +uuid={} +type=bluetooth + +[gsm] +apn=internet2.voicestream.com +device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 +home-only=true +network-id=254098 +password=parliament2 +pin=123456 +sim-id=89148000000060671234 +sim-operator-id=310260 +username=george.clinton.again + +[ipv4] +dns-search= +method=auto + +[ipv6] +dns-search= +method=auto + +[proxy]'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + nm-devices: + NM-{}: + renderer: NetworkManager + networkmanager: + uuid: "{}" + name: "T-Mobile Funkadelic 2" + passthrough: + connection.type: "bluetooth" + gsm.apn: "internet2.voicestream.com" + gsm.device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" + gsm.home-only: "true" + gsm.network-id: "254098" + gsm.password: "parliament2" + gsm.pin: "123456" + gsm.sim-id: "89148000000060671234" + gsm.sim-operator-id: "310260" + gsm.username: "george.clinton.again" + ipv4.dns-search: "" + ipv4.method: "auto" + ipv6.dns-search: "" + ipv6.method: "auto" + proxy._: "" +'''.format(UUID, UUID)}) + + def test_keyfile_method_auto(self): + self.generate_from_keyfile('''[connection] +id=Test +uuid={} +type=ethernet + +[ethernet] +wake-on-lan=0 +mtu=1500 +cloned-mac-address=00:11:22:33:44:55 + +[ipv4] +dns-search= +method=auto +ignore-auto-routes=true +never-default=true +route-metric=4242 + +[ipv6] +addr-gen-mode=eui64 +token=1234::3 +dns-search= +method=auto +ignore-auto-routes=true +never-default=true +route-metric=4242 + +[proxy] +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + ethernets: + NM-{}: + renderer: NetworkManager + match: {{}} + dhcp4: true + dhcp4-overrides: + use-routes: false + route-metric: 4242 + dhcp6: true + dhcp6-overrides: + use-routes: false + route-metric: 4242 + macaddress: "00:11:22:33:44:55" + ipv6-address-generation: "eui64" + ipv6-address-token: "1234::3" + mtu: 1500 + networkmanager: + uuid: "{}" + name: "Test" + passthrough: + proxy._: "" +'''.format(UUID, UUID)}) + + def test_keyfile_method_manual(self): + self.generate_from_keyfile('''[connection] +id=Test +uuid={} +type=ethernet + +[ethernet] +mac-address=00:11:22:33:44:55 + +[ipv4] +dns-search=foo.local;bar.remote; +dns=9.8.7.6;5.4.3.2 +method=manual +address1=1.2.3.4/24,8.8.8.8 +address2=5.6.7.8/16 +gateway=6.6.6.6 +route1=1.1.2.2/16,8.8.8.8,42 +route1_options=onlink=true,initrwnd=33,initcwnd=44,mtu=1024,table=102,src=10.10.10.11 +route2=2.2.3.3/24,4.4.4.4 + +[ipv6] +addr-gen-mode=stable-privacy +dns-search=bar.local +dns=dead:beef::2; +method=manual +address1=1:2:3::9/128 +gateway=6:6::6 +route1=dead:beef::1/128,2001:1234::2 +route1_options=unknown=invalid, + +[proxy] +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + ethernets: + NM-{}: + renderer: NetworkManager + match: + macaddress: "00:11:22:33:44:55" + addresses: + - "1.2.3.4/24" + - "5.6.7.8/16" + - "1:2:3::9/128" + nameservers: + addresses: + - 9.8.7.6 + - 5.4.3.2 + - dead:beef::2 + search: + - foo.local + - bar.remote + - bar.local + gateway4: 6.6.6.6 + gateway6: 6:6::6 + ipv6-address-generation: "stable-privacy" + routes: + - metric: 42 + table: 102 + mtu: 1024 + congestion-window: 44 + advertised-receive-window: 33 + on-link: "true" + from: "10.10.10.11" + to: "1.1.2.2/16" + via: "8.8.8.8" + - to: "2.2.3.3/24" + via: "4.4.4.4" + - to: "dead:beef::1/128" + via: "2001:1234::2" + wakeonlan: true + networkmanager: + uuid: "{}" + name: "Test" + passthrough: + ipv4.method: "manual" + ipv4.address1: "1.2.3.4/24,8.8.8.8" + ipv6.route1: "dead:beef::1/128,2001:1234::2" + ipv6.route1_options: "unknown=invalid," + proxy._: "" +'''.format(UUID, UUID)}) + + def _template_keyfile_type(self, nd_type, nm_type, supported=True): + self.maxDiff = None + file = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(file)) + with open(file, 'w') as f: + f.write('[connection]\ntype={}\nuuid={}'.format(nm_type, UUID)) + self.assertEqual(lib.netplan_clear_netdefs(), 0) + lib.netplan_parse_keyfile(file.encode(), None) + lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + t = '\n passthrough:\n connection.type: "{}"'.format(nm_type) if not supported else '' + match = '\n match: {}' if nd_type in ['ethernets', 'modems', 'wifis'] else '' + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + {}: + NM-{}: + renderer: NetworkManager{} + networkmanager: + uuid: "{}"{} +'''.format(nd_type, UUID, match, UUID, t)) + + def test_keyfile_ethernet(self): + self._template_keyfile_type('ethernets', 'ethernet') + + def test_keyfile_type_modem_gsm(self): + self._template_keyfile_type('modems', 'gsm') + + def test_keyfile_type_modem_cdma(self): + self._template_keyfile_type('modems', 'cdma') + + def test_keyfile_type_bridge(self): + self._template_keyfile_type('bridges', 'bridge') + + def test_keyfile_type_bond(self): + self._template_keyfile_type('bonds', 'bond') + + def test_keyfile_type_vlan(self): + self._template_keyfile_type('vlans', 'vlan') + + def test_keyfile_type_tunnel(self): + self._template_keyfile_type('tunnels', 'ip-tunnel', False) + + def test_keyfile_type_wireguard(self): + self._template_keyfile_type('tunnels', 'wireguard', False) + + def test_keyfile_type_other(self): + self._template_keyfile_type('nm-devices', 'dummy', False) + + def test_keyfile_type_wifi(self): + self.generate_from_keyfile('''[connection] +type=wifi +uuid={} +permissions= +id=myid with spaces +interface-name=eth0 + +[wifi] +ssid=SOME-SSID +mode=infrastructure +hidden=true +mtu=1500 +cloned-mac-address=00:11:22:33:44:55 +band=a +channel=12 +bssid=de:ad:be:ef:ca:fe + +[wifi-security] +key-mgmt=ieee8021x + +[ipv4] +method=auto +dns-search='''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: + name: "eth0" + dhcp4: true + macaddress: "00:11:22:33:44:55" + mtu: 1500 + access-points: + "SOME-SSID": + hidden: true + bssid: "de:ad:be:ef:ca:fe" + band: "5GHz" + channel: 12 + auth: + key-management: "802.1x" + networkmanager: + uuid: "{}" + name: "myid with spaces" + passthrough: + connection.permissions: "" + networkmanager: + uuid: "{}" + name: "myid with spaces" +'''.format(UUID, UUID, UUID)}) + + def _template_keyfile_type_wifi_eap(self, method): + self.generate_from_keyfile('''[connection] +type=wifi +uuid={} +permissions= +id=testnet +interface-name=wlan0 + +[wifi] +ssid=testnet +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-eap + +[802-1x] +eap={} +identity=some-id +anonymous-identity=anon-id +password=v3rys3cr3t! +ca-cert=/some/path.key +client-cert=/some/path.client_cert +private-key=/some/path.key +private-key-password=s0s3cr3t!!111 +phase2-auth=chap + +[ipv4] +method=auto +dns-search='''.format(UUID, method)) + self.assert_netplan({UUID: '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: + name: "wlan0" + dhcp4: true + access-points: + "testnet": + auth: + key-management: "eap" + method: "{}" + anonymous-identity: "anon-id" + identity: "some-id" + ca-certificate: "/some/path.key" + client-certificate: "/some/path.client_cert" + client-key: "/some/path.key" + client-key-password: "s0s3cr3t!!111" + phase2-auth: "chap" + password: "v3rys3cr3t!" + networkmanager: + uuid: "{}" + name: "testnet" + passthrough: + connection.permissions: "" + networkmanager: + uuid: "{}" + name: "testnet" +'''.format(UUID, method, UUID, UUID)}) + + def test_keyfile_type_wifi_eap_peap(self): + self._template_keyfile_type_wifi_eap('peap') + + def test_keyfile_type_wifi_eap_tls(self): + self._template_keyfile_type_wifi_eap('tls') + + def test_keyfile_type_wifi_eap_ttls(self): + self._template_keyfile_type_wifi_eap('ttls') + + def _template_keyfile_type_wifi(self, nd_mode, nm_mode): + self.generate_from_keyfile('''[connection] +type=wifi +uuid={} +id=myid with spaces + +[ipv4] +method=auto + +[wifi] +ssid=SOME-SSID +wake-on-wlan=24 +band=bg +mode={}'''.format(UUID, nm_mode)) + wifi_mode = '' + ap_mode = '' + if nm_mode != nd_mode: + wifi_mode = ''' + passthrough: + wifi.mode: "{}"'''.format(nm_mode) + if nd_mode != 'infrastructure': + ap_mode = '\n mode: "%s"' % nd_mode + self.assert_netplan({UUID: '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: {{}} + dhcp4: true + wakeonwlan: + - magic_pkt + - gtk_rekey_failure + access-points: + "SOME-SSID": + band: "2.4GHz"{} + networkmanager: + uuid: "{}" + name: "myid with spaces"{} + networkmanager: + uuid: "{}" + name: "myid with spaces" +'''.format(UUID, ap_mode, UUID, wifi_mode, UUID)}) + + def test_keyfile_type_wifi_ap(self): + self.generate_from_keyfile('''[connection] +type=wifi +uuid={} +id=myid with spaces + +[ipv4] +method=shared + +[wifi] +ssid=SOME-SSID +wake-on-wlan=24 +band=bg +mode=ap'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: {{}} + wakeonwlan: + - magic_pkt + - gtk_rekey_failure + access-points: + "SOME-SSID": + band: "2.4GHz" + mode: "ap" + networkmanager: + uuid: "{}" + name: "myid with spaces" + networkmanager: + uuid: "{}" + name: "myid with spaces" +'''.format(UUID, UUID, UUID)}) + + def test_keyfile_type_wifi_adhoc(self): + self._template_keyfile_type_wifi('adhoc', 'adhoc') + + def test_keyfile_type_wifi_unknown(self): + self._template_keyfile_type_wifi('infrastructure', 'mesh') + + def test_keyfile_type_wifi_missing_ssid(self): + err = self.generate_from_keyfile('''[connection]\ntype=wifi\nuuid={}\nid=myid with spaces'''.format(UUID), expect_fail=True) + self.assertFalse(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + self.assertIn('netplan: Keyfile: cannot find SSID for WiFi connection', err) + + def test_keyfile_wake_on_lan(self): + self.generate_from_keyfile('''[connection] +type=ethernet +uuid={} +id=myid with spaces + +[ethernet] +wake-on-lan=2 + +[ipv4] +method=auto'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + ethernets: + NM-{}: + renderer: NetworkManager + match: {{}} + dhcp4: true + wakeonlan: true + networkmanager: + uuid: "{}" + name: "myid with spaces" + passthrough: + ethernet.wake-on-lan: "2" +'''.format(UUID, UUID)}) + + def test_keyfile_wake_on_lan_nm_default(self): + self.generate_from_keyfile('''[connection] +type=ethernet +uuid={} +id=myid with spaces + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + ethernets: + NM-{}: + renderer: NetworkManager + match: {{}} + dhcp4: true + networkmanager: + uuid: "{}" + name: "myid with spaces" +'''.format(UUID, UUID)}) + + def test_keyfile_modem_gsm(self): + self.generate_from_keyfile('''[connection] +type=gsm +uuid={} +id=myid with spaces + +[ipv4] +method=auto + +[gsm] +auto-config=true'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + modems: + NM-{}: + renderer: NetworkManager + match: {{}} + dhcp4: true + auto-config: true + networkmanager: + uuid: "{}" + name: "myid with spaces" +'''.format(UUID, UUID)}) + + def test_keyfile_existing_id(self): + self.generate_from_keyfile('''[connection] +type=bridge +interface-name=mybr +uuid={} +id=renamed netplan bridge + +[ipv4] +method=auto'''.format(UUID), netdef_id='mybr') + self.assert_netplan({UUID: '''network: + version: 2 + bridges: + mybr: + renderer: NetworkManager + dhcp4: true + networkmanager: + uuid: "{}" + name: "renamed netplan bridge" +'''.format(UUID)}) + + def test_keyfile_yaml_wifi_hotspot(self): + self.generate_from_keyfile('''[connection] +id=Hotspot-1 +type=wifi +uuid={} +interface-name=wlan0 +autoconnect=false +permissions= + +[ipv4] +method=shared +dns-search= + +[ipv6] +method=ignore +addr-gen-mode=1 +dns-search= + +[wifi] +ssid=my-hotspot +mode=ap +mac-address-blacklist= + +[wifi-security] +group=ccmp; +key-mgmt=wpa-psk +pairwise=ccmp; +proto=rsn; +psk=test1234 + +[proxy]'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: + name: "wlan0" + access-points: + "my-hotspot": + auth: + key-management: "psk" + password: "test1234" + mode: "ap" + networkmanager: + uuid: "{}" + name: "Hotspot-1" + passthrough: + connection.autoconnect: "false" + connection.permissions: "" + ipv6.addr-gen-mode: "1" + wifi.mac-address-blacklist: "" + wifi-security.group: "ccmp;" + wifi-security.pairwise: "ccmp;" + wifi-security.proto: "rsn;" + proxy._: "" + networkmanager: + uuid: "{}" + name: "Hotspot-1" +'''.format(UUID, UUID, UUID)}) + + def test_keyfile_ip4_linklocal_ip6_ignore(self): + self.generate_from_keyfile('''[connection] +id=netplan-eth1 +type=ethernet +interface-name=eth1 +uuid={} + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + ethernets: + NM-{}: + renderer: NetworkManager + match: + name: "eth1" + networkmanager: + uuid: "{}" + name: "netplan-eth1" +'''.format(UUID, UUID)}) + + def test_keyfile_vlan(self): + self.generate_from_keyfile('''[connection] +id=netplan-enblue +type=vlan +interface-name=enblue +uuid={} + +[vlan] +id=1 +parent=en1 + +[ipv4] +method=manual +address1=1.2.3.4/24 + +[ipv6] +method=ignore +'''.format(UUID), netdef_id='enblue', expect_fail=False, filename="some.keyfile") + self.assert_netplan({UUID: '''network: + version: 2 + vlans: + enblue: + renderer: NetworkManager + addresses: + - "1.2.3.4/24" + id: 1 + networkmanager: + uuid: "{}" + name: "netplan-enblue" + passthrough: + vlan.parent: "en1" +'''.format(UUID)}) + + def test_keyfile_bridge(self): + self.generate_from_keyfile('''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 +uuid={} + +[bridge] +ageing-time=50 +priority=1000 +forward-delay=12 +hello-time=6 +max-age=24 +stp=false + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''.format(UUID), netdef_id='br0', expect_fail=False, filename="netplan-br0.nmconnection") + self.assert_netplan({UUID: '''network: + version: 2 + bridges: + br0: + renderer: NetworkManager + dhcp4: true + parameters: + ageing-time: "50" + forward-delay: "12" + hello-time: "6" + max-age: "24" + priority: 1000 + stp: false + networkmanager: + uuid: "{}" + name: "netplan-br0" +'''.format(UUID)}) + + def test_keyfile_bridge_default_stp(self): + self.generate_from_keyfile('''[connection] +id=netplan-br0 +type=bridge +interface-name=br0 +uuid={} + +[bridge] +hello-time=6 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''.format(UUID), netdef_id='br0') + self.assert_netplan({UUID: '''network: + version: 2 + bridges: + br0: + renderer: NetworkManager + dhcp4: true + parameters: + hello-time: "6" + networkmanager: + uuid: "{}" + name: "netplan-br0" +'''.format(UUID)}) + + def test_keyfile_bond(self): + self.generate_from_keyfile('''[connection] +uuid={} +id=netplan-bn0 +type=bond +interface-name=bn0 + +[bond] +mode=802.3ad +lacp_rate=10 +miimon=10 +min_links=10 +xmit_hash_policy=none +ad_select=none +all_slaves_active=1 +arp_interval=10 +arp_ip_target=10.10.10.10,20.20.20.20 +arp_validate=all +arp_all_targets=all +updelay=10 +downdelay=10 +fail_over_mac=none +num_grat_arp=10 +num_unsol_na=10 +packets_per_slave=10 +primary_reselect=none +resend_igmp=10 +lp_interval=10 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''.format(UUID), netdef_id='bn0') + self.assert_netplan({UUID: '''network: + version: 2 + bonds: + bn0: + renderer: NetworkManager + dhcp4: true + parameters: + mode: "802.3ad" + mii-monitor-interval: "10" + up-delay: "10" + down-delay: "10" + lacp-rate: "10" + transmit-hash-policy: "none" + ad-select: "none" + arp-validate: "all" + arp-all-targets: "all" + fail-over-mac-policy: "none" + primary-reselect-policy: "none" + learn-packet-interval: "10" + arp-interval: "10" + min-links: 10 + all-slaves-active: true + gratuitous-arp: 10 + packets-per-slave: 10 + resend-igmp: 10 + arp-ip-targets: + - 10.10.10.10 + - 20.20.20.20 + networkmanager: + uuid: "{}" + name: "netplan-bn0" +'''.format(UUID)}) + + def test_keyfile_customer_A1(self): + self.generate_from_keyfile('''[connection] +id=netplan-wlan0-TESTSSID +type=wifi +interface-name=wlan0 +uuid={} + +[ipv4] +method=auto + +[ipv6] +method=ignore + +[wifi] +ssid=TESTSSID +mode=infrastructure + +[wifi-security] +key-mgmt=wpa-psk +psk=s0s3cr1t +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: + name: "wlan0" + dhcp4: true + access-points: + "TESTSSID": + auth: + key-management: "psk" + password: "s0s3cr1t" + networkmanager: + uuid: "ff9d6ebc-226d-4f82-a485-b7ff83b9607f" + name: "netplan-wlan0-TESTSSID" + networkmanager: + uuid: "{}" + name: "netplan-wlan0-TESTSSID" +'''.format(UUID, UUID)}) + + def test_keyfile_customer_A2(self): + self.generate_from_keyfile('''[connection] +id=gsm +type=gsm +uuid={} +interface-name=cdc-wdm1 + +[gsm] +apn=internet + +[ipv4] +method=auto +address1=10.10.28.159/24 +address2=10.10.164.254/24 +address3=10.10.246.132/24 +dns=8.8.8.8;8.8.4.4;8.8.8.8;8.8.4.4;8.8.8.8;8.8.4.4; + +[ipv6] +method=auto +addr-gen-mode=1 +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + modems: + NM-{}: + renderer: NetworkManager + match: + name: "cdc-wdm1" + nameservers: + addresses: + - 8.8.8.8 + - 8.8.4.4 + - 8.8.8.8 + - 8.8.4.4 + - 8.8.8.8 + - 8.8.4.4 + dhcp4: true + dhcp6: true + apn: "internet" + networkmanager: + uuid: "{}" + name: "gsm" + passthrough: + ipv4.address1: "10.10.28.159/24" + ipv4.address2: "10.10.164.254/24" + ipv4.address3: "10.10.246.132/24" + ipv6.addr-gen-mode: "1" +'''.format(UUID, UUID)}) diff --git a/tests/test_cli_get_set.py b/tests/test_cli_get_set.py new file mode 100644 index 0000000..7a1799b --- /dev/null +++ b/tests/test_cli_get_set.py @@ -0,0 +1,386 @@ +#!/usr/bin/python3 +# Blackbox tests of netplan CLI. These are run during "make check" and don't +# touch the system configuration at all. +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import sys +import unittest +import tempfile +import io +import shutil + +from contextlib import redirect_stdout +from netplan.cli.core import Netplan + + +def _call_cli(args): + old_sys_argv = sys.argv + sys.argv = [old_sys_argv[0]] + args + try: + f = io.StringIO() + with redirect_stdout(f): + Netplan().main() + return f.getvalue() + except Exception as e: + return e + finally: + sys.argv = old_sys_argv + + +class TestSet(unittest.TestCase): + '''Test netplan set''' + def setUp(self): + self.workdir = tempfile.TemporaryDirectory(prefix='netplan_') + self.file = '70-netplan-set.yaml' + self.path = os.path.join(self.workdir.name, 'etc', 'netplan', self.file) + os.makedirs(os.path.join(self.workdir.name, 'etc', 'netplan')) + + def tearDown(self): + shutil.rmtree(self.workdir.name) + + def _set(self, args): + args.insert(0, 'set') + return _call_cli(args + ['--root-dir', self.workdir.name]) + + def test_set_scalar(self): + self._set(['ethernets.eth0.dhcp4=true']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + self.assertIn('network:\n ethernets:\n eth0:\n dhcp4: true', f.read()) + + def test_set_scalar2(self): + self._set(['ethernets.eth0.dhcp4="yes"']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + self.assertIn('network:\n ethernets:\n eth0:\n dhcp4: \'yes\'', f.read()) + + def test_set_global(self): + self._set([r'network={renderer: NetworkManager}']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + self.assertIn('network:\n renderer: NetworkManager', f.read()) + + def test_set_sequence(self): + self._set(['ethernets.eth0.addresses=[1.2.3.4/24, \'5.6.7.8/24\']']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + self.assertIn('''network:\n ethernets:\n eth0: + addresses: + - 1.2.3.4/24 + - 5.6.7.8/24''', f.read()) + + def test_set_sequence2(self): + self._set(['ethernets.eth0.addresses=["1.2.3.4/24", 5.6.7.8/24]']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + self.assertIn('''network:\n ethernets:\n eth0: + addresses: + - 1.2.3.4/24 + - 5.6.7.8/24''', f.read()) + + def test_set_mapping(self): + self._set(['ethernets.eth0={addresses: [1.2.3.4/24], dhcp4: true}']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + self.assertIn('''network:\n ethernets:\n eth0: + addresses: + - 1.2.3.4/24 + dhcp4: true''', f.read()) + + def test_set_origin_hint(self): + self._set(['ethernets.eth0.dhcp4=true', '--origin-hint=99_snapd']) + p = os.path.join(self.workdir.name, 'etc', 'netplan', '99_snapd.yaml') + self.assertTrue(os.path.isfile(p)) + with open(p, 'r') as f: + self.assertEquals('network:\n ethernets:\n eth0:\n dhcp4: true\n', f.read()) + + def test_set_empty_origin_hint(self): + err = self._set(['ethernets.eth0.dhcp4=true', '--origin-hint=']) + self.assertIsInstance(err, Exception) + self.assertIn('Invalid/empty origin-hint', str(err)) + + def test_set_invalid(self): + err = self._set(['xxx.yyy=abc']) + self.assertIsInstance(err, Exception) + self.assertIn('unknown key \'xxx\'\n xxx:\n', str(err)) + self.assertFalse(os.path.isfile(self.path)) + + def test_set_invalid_validation(self): + err = self._set(['ethernets.eth0.set-name=myif0']) + self.assertIsInstance(err, Exception) + self.assertIn('eth0: \'set-name:\' requires \'match:\' properties', str(err)) + self.assertFalse(os.path.isfile(self.path)) + + def test_set_invalid_validation2(self): + with open(self.path, 'w') as f: + f.write('''network: + tunnels: + tun0: + mode: sit + local: 1.2.3.4 + remote: 5.6.7.8''') + err = self._set(['tunnels.tun0.keys.input=12345']) + self.assertIsInstance(err, Exception) + self.assertIn('tun0: \'input-key\' is not required for this tunnel type', str(err)) + + def test_set_append(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + ens3: {dhcp4: yes}''') + self._set(['ethernets.eth0.dhcp4=true']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + out = f.read() + self.assertIn('network:\n ethernets:\n', out) + self.assertIn(' ens3:\n dhcp4: true', out) + self.assertIn(' eth0:\n dhcp4: true', out) + self.assertIn(' version: 2', out) + + def test_set_overwrite_eq(self): + with open(self.path, 'w') as f: + f.write('''network: + ethernets: + ens3: {dhcp4: "yes"}''') + self._set(['ethernets.ens3.dhcp4=yes']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + out = f.read() + self.assertIn('network:\n ethernets:\n', out) + self.assertIn(' ens3:\n dhcp4: true', out) + + def test_set_overwrite(self): + with open(self.path, 'w') as f: + f.write('''network: + ethernets: + ens3: {dhcp4: "yes"}''') + self._set(['ethernets.ens3.dhcp4=true']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + out = f.read() + self.assertIn('network:\n ethernets:\n', out) + self.assertIn(' ens3:\n dhcp4: true', out) + + def test_set_delete(self): + with open(self.path, 'w') as f: + f.write('''network:\n version: 2\n renderer: NetworkManager + ethernets: + ens3: {dhcp4: yes, dhcp6: yes} + eth0: {addresses: [1.2.3.4/24]}''') + self._set(['ethernets.eth0.addresses=NULL']) + self._set(['ethernets.ens3.dhcp6=null']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + out = f.read() + self.assertIn('network:\n ethernets:\n', out) + self.assertIn(' version: 2', out) + self.assertIn(' ens3:\n dhcp4: true', out) + self.assertNotIn('dhcp6: true', out) + self.assertNotIn('addresses:', out) + self.assertNotIn('eth0:', out) + + def test_set_delete_file(self): + with open(self.path, 'w') as f: + f.write('''network: + ethernets: + ens3: {dhcp4: yes}''') + self._set(['network.ethernets.ens3.dhcp4=NULL']) + # The file should be deleted if this was the last/only key left + self.assertFalse(os.path.isfile(self.path)) + + def test_set_delete_file_with_version(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + ens3: {dhcp4: yes}''') + out = self._set(['network.ethernets.ens3=NULL']) + print(out, flush=True) + # The file should be deleted if only "network: {version: 2}" is left + self.assertFalse(os.path.isfile(self.path)) + + def test_set_invalid_delete(self): + with open(self.path, 'w') as f: + f.write('''network:\n version: 2\n renderer: NetworkManager + ethernets: + eth0: {addresses: [1.2.3.4]}''') + err = self._set(['ethernets.eth0.addresses']) + self.assertIsInstance(err, Exception) + self.assertEquals('Invalid value specified', str(err)) + + def test_set_escaped_dot(self): + self._set([r'ethernets.eth0\.123.dhcp4=false']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + self.assertIn('network:\n ethernets:\n eth0.123:\n dhcp4: false', f.read()) + + def test_set_invalid_input(self): + err = self._set([r'ethernets.eth0={dhcp4:false}']) + self.assertIsInstance(err, Exception) + self.assertEquals('Invalid input: {\'network\': {\'ethernets\': {\'eth0\': {\'dhcp4:false\': None}}}}', str(err)) + + def test_set_override_existing_file(self): + override = os.path.join(self.workdir.name, 'etc', 'netplan', 'some-file.yaml') + with open(override, 'w') as f: + f.write(r'network: {ethernets: {eth0: {dhcp4: true}, eth1: {dhcp6: false}}}') + self._set([r'ethernets.eth0.dhcp4=false']) + self.assertFalse(os.path.isfile(self.path)) + self.assertTrue(os.path.isfile(override)) + with open(override, 'r') as f: + out = f.read() + self.assertIn('network:\n ethernets:\n eth0:\n dhcp4: false', out) # new + self.assertIn('eth1:\n dhcp6: false', out) # old + + def test_set_override_existing_file_escaped_dot(self): + override = os.path.join(self.workdir.name, 'etc', 'netplan', 'some-file.yaml') + with open(override, 'w') as f: + f.write(r'network: {ethernets: {eth0.123: {dhcp4: true}}}') + self._set([r'ethernets.eth0\.123.dhcp4=false']) + self.assertFalse(os.path.isfile(self.path)) + self.assertTrue(os.path.isfile(override)) + with open(override, 'r') as f: + self.assertIn('network:\n ethernets:\n eth0.123:\n dhcp4: false', f.read()) + + def test_set_override_multiple_existing_files(self): + file1 = os.path.join(self.workdir.name, 'etc', 'netplan', 'eth0.yaml') + with open(file1, 'w') as f: + f.write(r'network: {ethernets: {eth0.1: {dhcp4: true}, eth0.2: {dhcp4: true}}}') + file2 = os.path.join(self.workdir.name, 'etc', 'netplan', 'eth1.yaml') + with open(file2, 'w') as f: + f.write(r'network: {ethernets: {eth1: {dhcp4: true}}}') + self._set([(r'network={renderer: NetworkManager, version: 2,' + r'ethernets:{' + r'eth1:{dhcp4: false},' + r'eth0.1:{dhcp4: false},' + r'eth0.2:{dhcp4: false}},' + r'bridges:{' + r'br99:{dhcp4: false}}}')]) + self.assertTrue(os.path.isfile(file1)) + with open(file1, 'r') as f: + self.assertIn('network:\n ethernets:\n eth0.1:\n dhcp4: false', f.read()) + self.assertTrue(os.path.isfile(file2)) + with open(file2, 'r') as f: + self.assertIn('network:\n ethernets:\n eth1:\n dhcp4: false', f.read()) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + out = f.read() + self.assertIn('network:\n bridges:\n br99:\n dhcp4: false', out) + self.assertIn(' version: 2', out) + self.assertIn(' renderer: NetworkManager', out) + + +class TestGet(unittest.TestCase): + '''Test netplan get''' + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + self.file = '00-config.yaml' + self.path = os.path.join(self.workdir.name, 'etc', 'netplan', self.file) + os.makedirs(os.path.join(self.workdir.name, 'etc', 'netplan')) + + def _get(self, args): + args.insert(0, 'get') + return _call_cli(args + ['--root-dir', self.workdir.name]) + + def test_get_scalar(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + ens3: {dhcp4: yes}''') + out = self._get(['ethernets.ens3.dhcp4']) + self.assertIn('true', out) + + def test_get_mapping(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + ens3: + dhcp4: yes + addresses: [1.2.3.4/24, 5.6.7.8/24]''') + out = self._get(['ethernets']) + self.assertIn('''ens3: + addresses: + - 1.2.3.4/24 + - 5.6.7.8/24 + dhcp4: true''', out) + + def test_get_modems(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + modems: + wwan0: + apn: internet + pin: 1234 + dhcp4: yes + addresses: [1.2.3.4/24, 5.6.7.8/24]''') + out = self._get(['modems.wwan0']) + self.assertIn('''addresses: +- 1.2.3.4/24 +- 5.6.7.8/24 +apn: internet +dhcp4: true +pin: 1234''', out) + + def test_get_sequence(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + ens3: {addresses: [1.2.3.4/24, 5.6.7.8/24]}''') + out = self._get(['network.ethernets.ens3.addresses']) + self.assertIn('- 1.2.3.4/24\n- 5.6.7.8/24', out) + + def test_get_null(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + ens3: {dhcp4: yes}''') + out = self._get(['ethernets.eth0.dhcp4']) + self.assertEqual('null\n', out) + + def test_get_escaped_dot(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0.123: {dhcp4: yes}''') + out = self._get([r'ethernets.eth0\.123.dhcp4']) + self.assertEquals('true\n', out) + + def test_get_all(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + eth0: {dhcp4: yes}''') + out = self._get([]) + self.assertEquals('''network: + ethernets: + eth0: + dhcp4: true + version: 2\n''', out) + + def test_get_network(self): + with open(self.path, 'w') as f: + f.write('network:\n version: 2\n renderer: NetworkManager') + out = self._get(['network']) + self.assertEquals('renderer: NetworkManager\nversion: 2\n', out) diff --git a/tests/test_cli_units.py b/tests/test_cli_units.py new file mode 100644 index 0000000..0814c18 --- /dev/null +++ b/tests/test_cli_units.py @@ -0,0 +1,41 @@ +#!/usr/bin/python3 +# Blackbox tests of netplan CLI. These are run during "make check" and don't +# touch the system configuration at all. +# +# Copyright (C) 2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from netplan.cli.commands.apply import NetplanApply + + +class TestCLI(unittest.TestCase): + '''Netplan CLI unittests''' + + def test_is_composite_member(self): + res = NetplanApply.is_composite_member([{'br0': {'interfaces': ['eth0']}}], 'eth0') + self.assertTrue(res) + + def test_is_composite_member_false(self): + res = NetplanApply.is_composite_member([ + {'br0': {'interfaces': ['eth42']}}, + {'bond0': {'interfaces': ['eth1']}} + ], 'eth0') + self.assertFalse(res) + + def test_is_composite_member_with_renderer(self): + res = NetplanApply.is_composite_member([{'renderer': 'networkd', 'br0': {'interfaces': ['eth0']}}], 'eth0') + self.assertTrue(res) diff --git a/tests/test_configmanager.py b/tests/test_configmanager.py new file mode 100644 index 0000000..e910f4e --- /dev/null +++ b/tests/test_configmanager.py @@ -0,0 +1,251 @@ +#!/usr/bin/python3 +# Validate ConfigManager methods +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import shutil +import tempfile +import unittest + +from netplan.configmanager import ConfigManager + + +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" + nm-devices: + fallback: + renderer: NetworkManager + networkmanager: + passthrough: + connection.id: some-nm-id + connection.uuid: some-uuid +''', 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) + self.assertIn('fallback', self.configmanager.nm_devices) + + 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) diff --git a/tests/test_libnetplan.py b/tests/test_libnetplan.py new file mode 100644 index 0000000..b4136c8 --- /dev/null +++ b/tests/test_libnetplan.py @@ -0,0 +1,153 @@ +#!/usr/bin/python3 +# Blackbox tests of certain libnetplan functions. These are run during +# "make check" and don't touch the system configuration at all. +# +# Copyright (C) 2020-2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import shutil +import ctypes +import ctypes.util + +from generator.base import TestBase +from parser.base import capture_stderr +from tests.test_utils import MockCmd + +rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +exe_cli = os.path.join(rootdir, 'src', 'netplan.script') +# Make sure we can import our development netplan. +os.environ.update({'PYTHONPATH': '.'}) + +lib = ctypes.CDLL(ctypes.util.find_library('netplan')) +lib.netplan_get_id_from_nm_filename.restype = ctypes.c_char_p + + +class TestLibnetplan(TestBase): + '''Test libnetplan functionality as used by the NetworkManager backend''' + + def setUp(self): + super().setUp() + os.makedirs(self.confdir) + + def tearDown(self): + shutil.rmtree(self.workdir.name) + super().tearDown() + + def test_get_id_from_filename(self): + out = lib.netplan_get_id_from_nm_filename( + '/run/NetworkManager/system-connections/netplan-some-id.nmconnection'.encode(), None) + self.assertEqual(out, b'some-id') + + def test_get_id_from_filename_rootdir(self): + out = lib.netplan_get_id_from_nm_filename( + '/some/rootdir/run/NetworkManager/system-connections/netplan-some-id.nmconnection'.encode(), None) + self.assertEqual(out, b'some-id') + + def test_get_id_from_filename_wifi(self): + out = lib.netplan_get_id_from_nm_filename( + '/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID.nmconnection'.encode(), 'SOME-SSID'.encode()) + self.assertEqual(out, b'some-id') + + def test_get_id_from_filename_wifi_invalid_suffix(self): + out = lib.netplan_get_id_from_nm_filename( + '/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID'.encode(), 'SOME-SSID'.encode()) + self.assertEqual(out, None) + + def test_get_id_from_filename_invalid_prefix(self): + out = lib.netplan_get_id_from_nm_filename('INVALID/netplan-some-id.nmconnection'.encode(), None) + self.assertEqual(out, None) + + def test_parse_keyfile_missing(self): + f = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(f)) + with capture_stderr() as outf: + self.assertFalse(lib.netplan_parse_keyfile(f.encode(), None)) + with open(outf.name, 'r') as f: + self.assertIn('netplan: cannot load keyfile', f.read().strip()) + + def test_generate(self): + self.mock_netplan_cmd = MockCmd("netplan") + os.environ["TEST_NETPLAN_CMD"] = self.mock_netplan_cmd.path + self.assertTrue(lib.netplan_generate(self.workdir.name.encode())) + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "generate", "--root-dir", self.workdir.name], + ]) + + def test_delete_connection(self): + os.environ["TEST_NETPLAN_CMD"] = exe_cli + orig = os.path.join(self.confdir, 'some-filename.yaml') + with open(orig, 'w') as f: + f.write('''network: + ethernets: + some-netplan-id: + dhcp4: true''') + self.assertTrue(os.path.isfile(orig)) + # Parse all YAML and delete 'some-netplan-id' connection file + self.assertTrue(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) + self.assertFalse(os.path.isfile(orig)) + + def test_delete_connection_id_not_found(self): + orig = os.path.join(self.confdir, 'some-filename.yaml') + with open(orig, 'w') as f: + f.write('''network: + ethernets: + some-netplan-id: + dhcp4: true''') + self.assertTrue(os.path.isfile(orig)) + with capture_stderr() as outf: + self.assertFalse(lib.netplan_delete_connection('unknown-id'.encode(), self.workdir.name.encode())) + self.assertTrue(os.path.isfile(orig)) + with open(outf.name, 'r') as f: + self.assertIn('netplan_delete_connection: Cannot delete unknown-id, does not exist.', f.read().strip()) + + def test_delete_connection_two_in_file(self): + os.environ["TEST_NETPLAN_CMD"] = exe_cli + orig = os.path.join(self.confdir, 'some-filename.yaml') + with open(orig, 'w') as f: + f.write('''network: + ethernets: + some-netplan-id: + dhcp4: true + other-id: + dhcp6: true''') + self.assertTrue(os.path.isfile(orig)) + self.assertTrue(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) + self.assertTrue(os.path.isfile(orig)) + # Verify the file still exists and still contains the other connection + with open(orig, 'r') as f: + self.assertEquals(f.read(), 'network:\n ethernets:\n other-id:\n dhcp6: true\n') + + def test_write_netplan_conf(self): + netdef_id = 'some-netplan-id' + orig = os.path.join(self.confdir, 'some-filename.yaml') + generated = os.path.join(self.confdir, '10-netplan-{}.yaml'.format(netdef_id)) + with open(orig, 'w') as f: + f.write('''network: + version: 2 + ethernets: + some-netplan-id: + renderer: networkd + match: + name: "eth42" +''') + # Parse YAML and and re-write the specified netdef ID into a new file + self.assertTrue(lib.netplan_parse_yaml(orig.encode(), None)) + lib._write_netplan_conf(netdef_id.encode(), self.workdir.name.encode()) + self.assertEqual(lib.netplan_clear_netdefs(), 1) + self.assertTrue(os.path.isfile(generated)) + with open(orig, 'r') as f: + with open(generated, 'r') as new: + self.assertEquals(f.read(), new.read()) diff --git a/tests/test_ovs.py b/tests/test_ovs.py new file mode 100644 index 0000000..393061d --- /dev/null +++ b/tests/test_ovs.py @@ -0,0 +1,148 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest + +from unittest.mock import patch, call +from netplan.cli.ovs import OPENVSWITCH_OVS_VSCTL as OVS + +import netplan.cli.ovs as ovs + + +class TestOVS(unittest.TestCase): + + @patch('subprocess.check_call') + def test_clear_settings_tag(self, mock): + ovs.clear_setting('Bridge', 'ovs0', 'netplan/external-ids/key', 'value') + mock.assert_called_with([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/external-ids/key']) + + @patch('subprocess.check_output') + @patch('subprocess.check_call') + def test_clear_global_ssl(self, mock, mock_out): + mock_out.return_value = ''' +Private key: /private/key.pem +Certificate: /another/cert.pem +CA Certificate: /some/ca-cert.pem +Bootstrap: false''' + ovs.clear_setting('Open_vSwitch', '.', 'netplan/global/set-ssl', '/private/key.pem,/another/cert.pem,/some/ca-cert.pem') + mock_out.assert_called_once_with([OVS, 'get-ssl'], universal_newlines=True) + mock.assert_has_calls([ + call([OVS, 'del-ssl']), + call([OVS, 'remove', 'Open_vSwitch', '.', 'external-ids', 'netplan/global/set-ssl']) + ]) + + @patch('subprocess.check_output') + @patch('subprocess.check_call') + def test_no_clear_global_ssl_different(self, mock, mock_out): + mock_out.return_value = ''' +Private key: /private/key.pem +Certificate: /another/cert.pem +CA Certificate: /some/ca-cert.pem +Bootstrap: false''' + ovs.clear_setting('Open_vSwitch', '.', 'netplan/global/set-ssl', '/some/key.pem,/other/cert.pem,/some/cert.pem') + mock_out.assert_called_once_with([OVS, 'get-ssl'], universal_newlines=True) + mock.assert_has_calls([ + call([OVS, 'remove', 'Open_vSwitch', '.', 'external-ids', 'netplan/global/set-ssl']) + ]) + + def test_clear_global_unknown(self): + with self.assertRaises(Exception): + ovs.clear_setting('Bridge', 'ovs0', 'netplan/global/set-something', 'INVALID') + + @patch('subprocess.check_output') + @patch('subprocess.check_call') + def test_clear_global(self, mock, mock_out): + mock_out.return_value = 'tcp:127.0.0.1:1337\nunix:/some/socket' + ovs.clear_setting('Bridge', 'ovs0', 'netplan/global/set-controller', 'tcp:127.0.0.1:1337,unix:/some/socket') + mock_out.assert_called_once_with([OVS, 'get-controller', 'ovs0'], universal_newlines=True) + mock.assert_has_calls([ + call([OVS, 'del-controller', 'ovs0']), + call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/global/set-controller']) + ]) + + @patch('subprocess.check_output') + @patch('subprocess.check_call') + def test_no_clear_global_different(self, mock, mock_out): + mock_out.return_value = 'unix:/var/run/openvswitch/ovs0.mgmt' + ovs.clear_setting('Bridge', 'ovs0', 'netplan/global/set-controller', 'tcp:127.0.0.1:1337,unix:/some/socket') + mock_out.assert_called_once_with([OVS, 'get-controller', 'ovs0'], universal_newlines=True) + mock.assert_has_calls([ + call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/global/set-controller']) + ]) + + @patch('subprocess.check_call') + def test_clear_dict(self, mock): + ovs.clear_setting('Bridge', 'ovs0', 'netplan/other-config/key', 'value') + mock.assert_has_calls([ + call([OVS, 'remove', 'Bridge', 'ovs0', 'other-config', 'key', 'value']), + call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/other-config/key']) + ]) + + @patch('subprocess.check_call') + def test_clear_col(self, mock): + ovs.clear_setting('Port', 'bond0', 'netplan/bond_mode', 'balance-tcp') + mock.assert_has_calls([ + call([OVS, 'remove', 'Port', 'bond0', 'bond_mode', 'balance-tcp']), + call([OVS, 'remove', 'Port', 'bond0', 'external-ids', 'netplan/bond_mode']) + ]) + + @patch('subprocess.check_call') + def test_clear_col_default(self, mock): + ovs.clear_setting('Bridge', 'ovs0', 'netplan/rstp_enable', 'true') + mock.assert_has_calls([ + call([OVS, 'set', 'Bridge', 'ovs0', 'rstp_enable=false']), + call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/rstp_enable']) + ]) + + @patch('subprocess.check_call') + def test_clear_dict_colon(self, mock): + ovs.clear_setting('Bridge', 'ovs0', 'netplan/other-config/key', 'fa:16:3e:4b:19:3a') + mock.assert_has_calls([ + call([OVS, 'remove', 'Bridge', 'ovs0', 'other-config', 'key', r'fa\:16\:3e\:4b\:19\:3a']), + call([OVS, 'remove', 'Bridge', 'ovs0', 'external-ids', 'netplan/other-config/key']) + ]) + mock.mock_calls + + def test_is_ovs_interface(self): + interfaces = dict() + interfaces['ovs0'] = {'openvswitch': {'set-fail-mode': 'secure'}} + self.assertTrue(ovs.is_ovs_interface('ovs0', interfaces)) + + def test_is_ovs_interface_false(self): + interfaces = dict() + interfaces['br0'] = {'interfaces': ['eth0', 'eth1']} + interfaces['eth0'] = {} + interfaces['eth1'] = {} + self.assertFalse(ovs.is_ovs_interface('br0', interfaces)) + + def test_is_ovs_interface_recursive(self): + interfaces = dict() + interfaces['patchx'] = {'peer': 'patchy', 'openvswitch': {}} + interfaces['patchy'] = {'peer': 'patchx', 'openvswitch': {}} + interfaces['ovs0'] = {'interfaces': ['bond0']} + interfaces['bond0'] = {'interfaces': ['patchx', 'patchy']} + self.assertTrue(ovs.is_ovs_interface('ovs0', interfaces)) + + def test_is_ovs_interface_invalid_key(self): + interfaces = dict() + interfaces['ovs0'] = {'openvswitch': {'set-fail-mode': 'secure'}} + self.assertFalse(ovs.is_ovs_interface('gretap1', interfaces)) + + def test_is_ovs_interface_special_key(self): + interfaces = dict() + interfaces['renderer'] = 'NetworkManager' + self.assertFalse(ovs.is_ovs_interface('renderer', interfaces)) diff --git a/tests/test_sriov.py b/tests/test_sriov.py new file mode 100644 index 0000000..4e8b834 --- /dev/null +++ b/tests/test_sriov.py @@ -0,0 +1,648 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Łukasz 'sil2100' Zemczak +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import unittest +import tempfile + +from subprocess import CalledProcessError +from collections import defaultdict +from unittest.mock import patch, mock_open, call + +import netplan.cli.sriov as sriov + +from netplan.configmanager import ConfigManager, ConfigurationError + + +class MockSRIOVOpen(): + def __init__(self): + # now this is a VERY ugly hack to make mock_open() better + self.read_queue = [] + self.write_queue = [] + + def sriov_read(): + action = self.read_queue.pop(0) + if isinstance(action, str): + return action + else: + raise action + + def sriov_write(data): + if not self.write_queue: + return + action = self.write_queue.pop(0) + if isinstance(action, Exception): + raise action + + self.open = mock_open() + self.open.return_value.read.side_effect = sriov_read + self.open.return_value.write.side_effect = sriov_write + + +def mock_set_counts(interfaces, config_manager, vf_counts, active_vfs, active_pfs): + counts = {'enp1': 2, 'enp2': 1} + vfs = {'enp1s16f1': None, 'enp1s16f2': None, 'customvf1': None} + pfs = {'enp1': 'enp1', 'enpx': 'enp2'} + vf_counts.update(counts) + active_vfs.update(vfs) + active_pfs.update(pfs) + + +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_set_name(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 == 'enp1' 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: + match: + driver: foo + set-name: pf1 + enp8: + match: + name: enp[3-8] + set-name: pf2 + virtual-function-count: 7 + enp1s16f1: + link: enp1 + macaddress: 01:02:03:04:05:00 +''', file=fd) + self.configmanager.parse() + interfaces = ['pf1', '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 - + # we expect netplan to take into consideration the renamed interface + # names here + self.assertDictEqual( + vf_counts, + {'pf1': 1, 'enp8': 7}) + # also check if the vfs and pfs dictionaries got properly set + self.assertDictEqual( + vfs, + {'enp1s16f1': None}) + self.assertDictEqual( + pfs, + {'enp1': 'pf1', '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) + # 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(self.configmanager) + + # make sure config_manager.parse() has been called + self.assertTrue(self.configmanager.config) + # 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) + # 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(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) + # 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(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) + # 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(self.configmanager) + + self.assertIn('matched more than one interface for a VF device: customvf1', + str(e.exception)) diff --git a/tests/test_terminal.py b/tests/test_terminal.py new file mode 100644 index 0000000..680aa23 --- /dev/null +++ b/tests/test_terminal.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +# Validate Terminal handling +# +# Copyright (C) 2018 Canonical, Ltd. +# Author: Mathieu Trudel-Lapierre +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import fcntl +import sys +import os +import termios +import unittest + +import netplan.terminal + + +@unittest.skipUnless(sys.__stdin__.isatty(), "not supported when run from a script") +class TestTerminal(unittest.TestCase): + + def setUp(self): + self.terminal = netplan.terminal.Terminal(sys.stdin.fileno()) + + def test_echo(self): + self.terminal.disable_echo() + attrs = termios.tcgetattr(self.terminal.fd) + self.assertFalse(attrs[3] & termios.ECHO) + self.terminal.enable_echo() + attrs = termios.tcgetattr(self.terminal.fd) + self.assertTrue(attrs[3] & termios.ECHO) + + def test_nonblocking_io(self): + orig_flags = flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) + self.assertFalse(flags & os.O_NONBLOCK) + self.terminal.enable_nonblocking_io() + flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) + self.assertTrue(flags & os.O_NONBLOCK) + self.assertNotEquals(flags, orig_flags) + self.terminal.disable_nonblocking_io() + flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) + self.assertFalse(flags & os.O_NONBLOCK) + self.assertEquals(flags, orig_flags) + + def test_save(self): + self.terminal.enable_nonblocking_io() + flags = self.terminal.orig_flags + self.terminal.save() + self.terminal.disable_nonblocking_io() + self.assertNotEquals(flags, self.terminal.orig_flags) + self.terminal.reset() + flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) + self.assertEquals(flags, self.terminal.orig_flags) + self.terminal.disable_nonblocking_io() + self.terminal.save() + + def test_save_and_restore_with_dict(self): + self.terminal.enable_nonblocking_io() + orig_settings = dict() + self.terminal.save(orig_settings) + self.terminal.disable_nonblocking_io() + flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) + self.assertNotEquals(flags, orig_settings.get('flags')) + self.terminal.reset(orig_settings) + flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) + self.assertEquals(flags, orig_settings.get('flags')) + self.terminal.disable_nonblocking_io() + + def test_reset(self): + self.terminal.enable_nonblocking_io() + flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) + self.assertTrue(flags & os.O_NONBLOCK) + self.terminal.reset() + flags = fcntl.fcntl(self.terminal.fd, fcntl.F_GETFL) + self.assertFalse(flags & os.O_NONBLOCK) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..7b8fd6f --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,295 @@ +#!/usr/bin/python3 +# +# Copyright (C) 2020 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import unittest +import tempfile +import glob +import netifaces + +import netplan.cli.utils as utils +from unittest.mock import patch + + +DEVICES = ['eth0', 'eth1', 'ens3', 'ens4', 'br0'] + + +# Consider switching to something more standard, like MockProc +class MockCmd: + """MockCmd will mock a given command name and capture all calls to it""" + + def __init__(self, name): + self._tmp = tempfile.TemporaryDirectory() + self.name = name + self.path = os.path.join(self._tmp.name, name) + self.call_log = os.path.join(self._tmp.name, "call.log") + with open(self.path, "w") as fp: + fp.write("""#!/bin/bash +printf "%%s" "$(basename "$0")" >> %(log)s +printf '\\0' >> %(log)s + +for arg in "$@"; do + printf "%%s" "$arg" >> %(log)s + printf '\\0' >> %(log)s +done + +printf '\\0' >> %(log)s +""" % {'log': self.call_log}) + os.chmod(self.path, 0o755) + + def calls(self): + """ + calls() returns the calls to the given mock command in the form of + [ ["cmd", "call1-arg1"], ["cmd", "call2-arg1"], ... ] + """ + with open(self.call_log) as fp: + b = fp.read() + calls = [] + for raw_call in b.rstrip("\0\0").split("\0\0"): + call = raw_call.rstrip("\0") + calls.append(call.split("\0")) + return calls + + def set_output(self, output): + with open(self.path, "a") as fp: + fp.write("cat << EOF\n%s\nEOF" % output) + + def set_timeout(self, timeout_dsec=10): + with open(self.path, "a") as fp: + fp.write(""" +if [[ "$*" == *try* ]] +then + ACTIVE=1 + trap 'ACTIVE=0' SIGUSR1 + trap 'ACTIVE=0' SIGINT + while (( $ACTIVE > 0 )) && (( $ACTIVE <= {} )) + do + ACTIVE=$(($ACTIVE+1)) + sleep 0.1 + done +fi +""".format(timeout_dsec)) + + def set_returncode(self, returncode): + with open(self.path, "a") as fp: + fp.write("exit %d" % returncode) + + +class TestUtils(unittest.TestCase): + + def setUp(self): + self.workdir = tempfile.TemporaryDirectory() + os.makedirs(os.path.join(self.workdir.name, 'etc/netplan')) + os.makedirs(os.path.join(self.workdir.name, + 'run/NetworkManager/system-connections')) + + def _create_nm_keyfile(self, filename, ifname): + with open(os.path.join(self.workdir.name, + 'run/NetworkManager/system-connections/', filename), 'w') as f: + f.write('[connection]\n') + f.write('key=value\n') + f.write('interface-name=%s\n' % ifname) + f.write('key2=value2\n') + + def test_nm_interfaces(self): + self._create_nm_keyfile('netplan-test.nmconnection', 'eth0') + self._create_nm_keyfile('netplan-test2.nmconnection', 'eth1') + ifaces = utils.nm_interfaces(glob.glob(os.path.join(self.workdir.name, + 'run/NetworkManager/system-connections/*.nmconnection')), + DEVICES) + self.assertTrue('eth0' in ifaces) + self.assertTrue('eth1' in ifaces) + self.assertTrue(len(ifaces) == 2) + + def test_nm_interfaces_globbing(self): + self._create_nm_keyfile('netplan-test.nmconnection', 'eth?') + ifaces = utils.nm_interfaces(glob.glob(os.path.join(self.workdir.name, + 'run/NetworkManager/system-connections/*.nmconnection')), + DEVICES) + self.assertTrue('eth0' in ifaces) + self.assertTrue('eth1' in ifaces) + self.assertTrue(len(ifaces) == 2) + + def test_nm_interfaces_globbing2(self): + self._create_nm_keyfile('netplan-test.nmconnection', 'e*') + ifaces = utils.nm_interfaces(glob.glob(os.path.join(self.workdir.name, + 'run/NetworkManager/system-connections/*.nmconnection')), + DEVICES) + self.assertTrue('eth0' in ifaces) + self.assertTrue('eth1' in ifaces) + self.assertTrue('ens3' in ifaces) + self.assertTrue('ens4' in ifaces) + self.assertTrue(len(ifaces) == 4) + + def test_find_matching_iface_too_many(self): + # too many matches + iface = utils.find_matching_iface(DEVICES, {'name': 'e*'}) + self.assertEqual(iface, None) + + @patch('netplan.cli.utils.get_interface_macaddress') + def test_find_matching_iface(self, gim): + # we mock-out get_interface_macaddress to return useful values for the test + gim.side_effect = lambda x: '00:01:02:03:04:05' if x == 'eth1' else '00:00:00:00:00:00' + + match = {'name': 'e*', 'macaddress': '00:01:02:03:04:05'} + iface = utils.find_matching_iface(DEVICES, match) + self.assertEqual(iface, 'eth1') + + @patch('netplan.cli.utils.get_interface_driver_name') + def test_find_matching_iface_name_and_driver(self, gidn): + # we mock-out get_interface_driver_name to return useful values for the test + gidn.side_effect = lambda x: 'foo' if x == 'ens4' else 'bar' + + match = {'name': 'ens?', 'driver': 'f*'} + iface = utils.find_matching_iface(DEVICES, match) + self.assertEqual(iface, 'ens4') + + @patch('netifaces.ifaddresses') + def test_interface_macaddress(self, ifaddr): + ifaddr.side_effect = lambda _: {netifaces.AF_LINK: [{'addr': '00:01:02:03:04:05'}]} + self.assertEqual(utils.get_interface_macaddress('eth42'), '00:01:02:03:04:05') + + @patch('netifaces.ifaddresses') + def test_interface_macaddress_empty(self, ifaddr): + ifaddr.side_effect = lambda _: {} + self.assertEqual(utils.get_interface_macaddress('eth42'), '') + + def test_netplan_get_filename_by_id(self): + file_a = os.path.join(self.workdir.name, 'etc/netplan/a.yaml') + file_b = os.path.join(self.workdir.name, 'etc/netplan/b.yaml') + with open(file_a, 'w') as f: + f.write('network:\n ethernets:\n id_a:\n dhcp4: true') + with open(file_b, 'w') as f: + f.write('network:\n ethernets:\n id_b:\n dhcp4: true\n id_a:\n dhcp4: true') + # netdef:b can only be found in b.yaml + basename = os.path.basename(utils.netplan_get_filename_by_id('id_b', self.workdir.name)) + self.assertEqual(basename, 'b.yaml') + # netdef:a is defined in a.yaml, overriden by b.yaml + basename = os.path.basename(utils.netplan_get_filename_by_id('id_a', self.workdir.name)) + self.assertEqual(basename, 'b.yaml') + + def test_netplan_get_filename_by_id_no_files(self): + self.assertIsNone(utils.netplan_get_filename_by_id('some-id', self.workdir.name)) + + def test_netplan_get_filename_by_id_invalid(self): + file = os.path.join(self.workdir.name, 'etc/netplan/a.yaml') + with open(file, 'w') as f: + f.write('''network: + tunnels: + id_a: + mode: sit + local: 0.0.0.0 + remote: 0.0.0.0 + key: 0.0.0.0''') + self.assertIsNone(utils.netplan_get_filename_by_id('some-id', self.workdir.name)) + + def test_systemctl(self): + self.mock_systemctl = MockCmd('systemctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_systemctl.path) + os.pathsep + path_env + utils.systemctl('start', ['service1', 'service2']) + self.assertEquals(self.mock_systemctl.calls(), [['systemctl', 'start', '--no-block', 'service1', 'service2']]) + + def test_networkd_interfaces(self): + self.mock_networkctl = MockCmd('networkctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env + self.mock_networkctl.set_output(''' + 1 lo loopback carrier unmanaged + 2 ens3 ether routable configured + 3 wlan0 wlan routable configuring +174 wwan0 wwan off linger''') + res = utils.networkd_interfaces() + self.assertEquals(self.mock_networkctl.calls(), [['networkctl', '--no-pager', '--no-legend']]) + self.assertIn('wlan0', res) + self.assertIn('ens3', res) + + def test_networkctl_reconfigure(self): + self.mock_networkctl = MockCmd('networkctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env + utils.networkctl_reconfigure(['eth0', 'eth1']) + self.assertEquals(self.mock_networkctl.calls(), [ + ['networkctl', 'reload'], + ['networkctl', 'reconfigure', 'eth0', 'eth1'] + ]) + + def test_is_nm_snap_enabled(self): + self.mock_cmd = MockCmd('systemctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env + self.assertTrue(utils.is_nm_snap_enabled()) + self.assertEquals(self.mock_cmd.calls(), [ + ['systemctl', '--quiet', 'is-enabled', 'snap.network-manager.networkmanager.service'] + ]) + + def test_is_nm_snap_enabled_false(self): + self.mock_cmd = MockCmd('systemctl') + self.mock_cmd.set_returncode(1) + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env + self.assertFalse(utils.is_nm_snap_enabled()) + self.assertEquals(self.mock_cmd.calls(), [ + ['systemctl', '--quiet', 'is-enabled', 'snap.network-manager.networkmanager.service'] + ]) + + def test_systemctl_network_manager(self): + self.mock_cmd = MockCmd('systemctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env + utils.systemctl_network_manager('start') + self.assertEquals(self.mock_cmd.calls(), [ + ['systemctl', '--quiet', 'is-enabled', 'snap.network-manager.networkmanager.service'], + ['systemctl', 'start', '--no-block', 'snap.network-manager.networkmanager.service'] + ]) + + def test_systemctl_is_active(self): + self.mock_cmd = MockCmd('systemctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env + self.assertTrue(utils.systemctl_is_active('some.service')) + self.assertEquals(self.mock_cmd.calls(), [ + ['systemctl', '--quiet', 'is-active', 'some.service'] + ]) + + def test_systemctl_is_active_false(self): + self.mock_cmd = MockCmd('systemctl') + self.mock_cmd.set_returncode(1) + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env + self.assertFalse(utils.systemctl_is_active('some.service')) + self.assertEquals(self.mock_cmd.calls(), [ + ['systemctl', '--quiet', 'is-active', 'some.service'] + ]) + + def test_systemctl_daemon_reload(self): + self.mock_cmd = MockCmd('systemctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env + utils.systemctl_daemon_reload() + self.assertEquals(self.mock_cmd.calls(), [ + ['systemctl', 'daemon-reload'] + ]) + + def test_ip_addr_flush(self): + self.mock_cmd = MockCmd('ip') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_cmd.path) + os.pathsep + path_env + utils.ip_addr_flush('eth42') + self.assertEquals(self.mock_cmd.calls(), [ + ['ip', 'addr', 'flush', 'eth42'] + ]) diff --git a/tests/validate_docs.sh b/tests/validate_docs.sh new file mode 100755 index 0000000..d5ee07a --- /dev/null +++ b/tests/validate_docs.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# find everything that looks like +# {"driver", YAML_SCALAR_NODE,..., +# extract the thing in quotes. + +# sanity check: make sure none have disappeared, as might happen from a reformat. +count=$(sed -n 's/[ ]\+{"\([a-z0-9-]\+\)", YAML_[A-Z]\+_NODE.*/\1/p' src/parse.c | sort | wc -l) +# 144 is based on 0.99+da6f776 definitions, and should be updated periodically. +if [ $count -lt 144 ]; then + echo "ERROR: fewer YAML keys defined in src/parse.c than expected!" + echo " Has the file been reformatted or refactored? If so, modify" + echo " validate_docs.sh appropriately." + exit 1 +fi + +# iterate through the keys +for term in $(sed -n 's/[ ]\+{"\([a-z0-9-]\+\)", YAML_[A-Z]\+_NODE.*/\1/p' src/parse.c | sort | uniq); do + # it can be documented in the following ways. + # 1. "Properties for device type ``blah:`` + if egrep "## Properties for device type \`\`$term:\`\`" doc/netplan.md > /dev/null; then + continue + fi + + # 2. "[blah, ]``blah``[, ``blah2``]: (scalar|bool|...) + if egrep "\`\`$term\`\`.*\((scalar|bool|mapping|sequence of scalars|sequence of mappings|sequence of sequence of scalars)" doc/netplan.md > /dev/null; then + continue + fi + + # 3. we give a pass to network and version + if [[ $term = "network" ]] || [[ $term = "version" ]]; then + continue + fi + + # 4. search doesn't get a full description but it's good enough + if [[ $term = "search" ]]; then + continue + fi + + # 5. gratuit_i_ous arp gets a special note + if [[ $term = "gratuitious-arp" ]]; then + continue + fi + + echo ERROR: The key "$term" is defined in the parser but not documented. + exit 1 +done +echo "validate_docs: OK" -- cgit v1.2.3 From 9f7ff2da2b0d412ed9fcbc1c7f45d2bd3423852c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 4 Aug 2021 15:15:45 +0200 Subject: parse-nm: fix 32bit format string Gbp-Pq: Name 0001-parse-nm-fix-32bit-format-string.patch --- src/parse-nm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parse-nm.c b/src/parse-nm.c index 9b09e34..bf998b7 100644 --- a/src/parse-nm.c +++ b/src/parse-nm.c @@ -136,7 +136,7 @@ static void handle_bridge_uint(GKeyFile* kf, const gchar* key, NetplanNetDefinition* nd, char** dataptr) { if (g_key_file_get_uint64(kf, "bridge", key, NULL)) { nd->custom_bridging = TRUE; - *dataptr = g_strdup_printf("%lu", g_key_file_get_uint64(kf, "bridge", key, NULL)); + *dataptr = g_strdup_printf("%"G_GUINT64_FORMAT, g_key_file_get_uint64(kf, "bridge", key, NULL)); _kf_clear_key(kf, "bridge", key); } } -- cgit v1.2.3 From 5c8dfc059523432e626a1cba4c9f5b8076e0a272 Mon Sep 17 00:00:00 2001 From: Debian netplan Maintainers Date: Thu, 21 Oct 2021 11:19:25 +0200 Subject: autopkgtest-fixes Gbp-Pq: Name autopkgtest-fixes.patch --- tests/integration/base.py | 11 +++-------- tests/integration/ethernets.py | 5 ++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index 5042bf4..93ee722 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -75,7 +75,7 @@ class IntegrationTestsBase(unittest.TestCase): os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') + f.write('[keyfile]\nunmanaged-devices+=interface-name:en*,eth0,veth42,veth43,nptestsrv') subprocess.check_call(['netplan', 'apply']) subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) @@ -144,12 +144,6 @@ class IntegrationTestsBase(unittest.TestCase): universal_newlines=True) klass.dev_e2_client_mac = out.split()[2] - os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) - - # work around https://launchpad.net/bugs/1615044 - with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices=') - @classmethod def shutdown_devices(klass): '''Remove test devices''' @@ -432,9 +426,10 @@ class IntegrationTestsWifi(IntegrationTestsBase): klass.dev_w_ap = devs[0] klass.dev_w_client = devs[1] + os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) # don't let NM trample over our fake AP with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: - f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=%s\n' % klass.dev_w_ap) @classmethod def shutdown_devices(klass): diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index ce016da..865c0d4 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -252,9 +252,8 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests): %(ec)s: dhcp6: no accept-ra: yes - addresses: [ '192.168.1.100/24' ] - %(e2c)s: {}''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() + addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], []) def test_eth_dhcp6_off_no_accept_ra(self): -- cgit v1.2.3 From 68d8b27beadd360554d5aeed61efb85923b49277 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Thu, 21 Oct 2021 11:19:25 +0200 Subject: Fix ethernets test with network-manage 1.32.10 Origin: vendor Forwarded: no Last-Update: 2021-09-01 NetworkManager's udev rules are smarter about catching renamed veths now, so the udev rule the integration tests use to manage their veths need to list the names the veths are renamed to too. Last-Update: 2021-09-01 Gbp-Pq: Name nm-1.32.10-compat.patch --- tests/integration/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index 93ee722..1054059 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -70,7 +70,7 @@ class IntegrationTestsBase(unittest.TestCase): # ensure NM can manage our fake eths os.makedirs('/run/udev/rules.d', exist_ok=True) with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: - f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') + f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43|iface1|iface2", ENV{NM_UNMANAGED}="0"\n') subprocess.check_call(['udevadm', 'control', '--reload']) os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) -- cgit v1.2.3 From dce1f36bf209c9d5c3598c5c05bc028d7804e43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Fri, 1 Oct 2021 12:39:40 +0200 Subject: generate:dbus:util: glib 2.70 compat g_spawn_check_exit_status is deprecated since libglib 2.70 and replaced by g_spawn_check_wait_status Gbp-Pq: Name glib-2.70-compat.patch --- src/dbus.c | 10 +++++----- src/generate.c | 2 +- src/util.c | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dbus.c b/src/dbus.c index f0aa53a..74032dc 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -110,7 +110,7 @@ _try_accept(bool accept, sd_bus_message *m, NetplanData *d, sd_bus_error *ret_er * Check return code/errors. */ kill(d->try_pid, signal); waitpid(d->try_pid, &status, 0); - g_spawn_check_exit_status(status, &error); + g_spawn_check_wait_status(status, &error); if (error != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan try failed: %s", error->message); // LCOV_EXCL_LINE @@ -228,7 +228,7 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan apply: %s", err->message); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan apply failed: %s\nstdout: '%s'\nstderr: '%s'", @@ -257,7 +257,7 @@ method_generate(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan generate: %s", err->message); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan generate failed: %s\nstdout: '%s'\nstderr: '%s'", @@ -334,7 +334,7 @@ method_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan get: %s", err->message); // LCOV_EXCL_LINE - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan get failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE @@ -380,7 +380,7 @@ method_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan set %s: %s", config_delta, err->message); // LCOV_EXCL_LINE - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan set failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE diff --git a/src/generate.c b/src/generate.c index cccd47a..24e60a2 100644 --- a/src/generate.c +++ b/src/generate.c @@ -67,7 +67,7 @@ check_called_just_in_time() gint exit_code = 0; g_spawn_sync(NULL, (gchar**)argv2, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, &exit_code, NULL); /* return TRUE, if network.target is not yet active */ - return !g_spawn_check_exit_status(exit_code, NULL); + return !g_spawn_check_wait_status(exit_code, NULL); } g_free(output); return FALSE; diff --git a/src/util.c b/src/util.c index a4c0dba..5861e13 100644 --- a/src/util.c +++ b/src/util.c @@ -188,7 +188,7 @@ systemd_escape(char* string) gchar *argv[] = {"bin" "/" "systemd-escape", string, NULL}; g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &escaped, &stderrh, &exit_status, &err); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) { // LCOV_EXCL_START g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", string, exit_status, escaped, stderrh); -- cgit v1.2.3 From 1fe7f738819c4c563d82ee95cd7fb02f60539e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 4 Aug 2021 15:15:45 +0200 Subject: parse-nm: fix 32bit format string Gbp-Pq: Name 0001-parse-nm-fix-32bit-format-string.patch --- src/parse-nm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parse-nm.c b/src/parse-nm.c index 9b09e34..bf998b7 100644 --- a/src/parse-nm.c +++ b/src/parse-nm.c @@ -136,7 +136,7 @@ static void handle_bridge_uint(GKeyFile* kf, const gchar* key, NetplanNetDefinition* nd, char** dataptr) { if (g_key_file_get_uint64(kf, "bridge", key, NULL)) { nd->custom_bridging = TRUE; - *dataptr = g_strdup_printf("%lu", g_key_file_get_uint64(kf, "bridge", key, NULL)); + *dataptr = g_strdup_printf("%"G_GUINT64_FORMAT, g_key_file_get_uint64(kf, "bridge", key, NULL)); _kf_clear_key(kf, "bridge", key); } } -- cgit v1.2.3 From 2b0fe8d940558ca5eded0362aacceaa2e062c312 Mon Sep 17 00:00:00 2001 From: Debian netplan Maintainers Date: Fri, 22 Oct 2021 09:22:22 +0200 Subject: autopkgtest-fixes Gbp-Pq: Name autopkgtest-fixes.patch --- tests/integration/base.py | 11 +++-------- tests/integration/ethernets.py | 5 ++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index 5042bf4..93ee722 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -75,7 +75,7 @@ class IntegrationTestsBase(unittest.TestCase): os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') + f.write('[keyfile]\nunmanaged-devices+=interface-name:en*,eth0,veth42,veth43,nptestsrv') subprocess.check_call(['netplan', 'apply']) subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) @@ -144,12 +144,6 @@ class IntegrationTestsBase(unittest.TestCase): universal_newlines=True) klass.dev_e2_client_mac = out.split()[2] - os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) - - # work around https://launchpad.net/bugs/1615044 - with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices=') - @classmethod def shutdown_devices(klass): '''Remove test devices''' @@ -432,9 +426,10 @@ class IntegrationTestsWifi(IntegrationTestsBase): klass.dev_w_ap = devs[0] klass.dev_w_client = devs[1] + os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) # don't let NM trample over our fake AP with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: - f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=%s\n' % klass.dev_w_ap) @classmethod def shutdown_devices(klass): diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index ce016da..865c0d4 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -252,9 +252,8 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests): %(ec)s: dhcp6: no accept-ra: yes - addresses: [ '192.168.1.100/24' ] - %(e2c)s: {}''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() + addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], []) def test_eth_dhcp6_off_no_accept_ra(self): -- cgit v1.2.3 From 7baa1274a20d4c31f3bdfc8546090ac0323ebb39 Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Fri, 22 Oct 2021 09:22:22 +0200 Subject: Fix ethernets test with network-manage 1.32.10 Origin: vendor Forwarded: no Last-Update: 2021-09-01 NetworkManager's udev rules are smarter about catching renamed veths now, so the udev rule the integration tests use to manage their veths need to list the names the veths are renamed to too. Last-Update: 2021-09-01 Gbp-Pq: Name nm-1.32.10-compat.patch --- tests/integration/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index 93ee722..1054059 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -70,7 +70,7 @@ class IntegrationTestsBase(unittest.TestCase): # ensure NM can manage our fake eths os.makedirs('/run/udev/rules.d', exist_ok=True) with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: - f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') + f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43|iface1|iface2", ENV{NM_UNMANAGED}="0"\n') subprocess.check_call(['udevadm', 'control', '--reload']) os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) -- cgit v1.2.3 From eb1f5857b64be8e32e2cd19dace6f87263596e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Fri, 1 Oct 2021 12:39:40 +0200 Subject: generate:dbus:util: glib 2.70 compat g_spawn_check_exit_status is deprecated since libglib 2.70 and replaced by g_spawn_check_wait_status Gbp-Pq: Name glib-2.70-compat.patch --- src/dbus.c | 10 +++++----- src/generate.c | 2 +- src/util.c | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dbus.c b/src/dbus.c index f0aa53a..74032dc 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -110,7 +110,7 @@ _try_accept(bool accept, sd_bus_message *m, NetplanData *d, sd_bus_error *ret_er * Check return code/errors. */ kill(d->try_pid, signal); waitpid(d->try_pid, &status, 0); - g_spawn_check_exit_status(status, &error); + g_spawn_check_wait_status(status, &error); if (error != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan try failed: %s", error->message); // LCOV_EXCL_LINE @@ -228,7 +228,7 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan apply: %s", err->message); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan apply failed: %s\nstdout: '%s'\nstderr: '%s'", @@ -257,7 +257,7 @@ method_generate(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan generate: %s", err->message); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan generate failed: %s\nstdout: '%s'\nstderr: '%s'", @@ -334,7 +334,7 @@ method_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan get: %s", err->message); // LCOV_EXCL_LINE - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan get failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE @@ -380,7 +380,7 @@ method_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan set %s: %s", config_delta, err->message); // LCOV_EXCL_LINE - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan set failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE diff --git a/src/generate.c b/src/generate.c index cccd47a..24e60a2 100644 --- a/src/generate.c +++ b/src/generate.c @@ -67,7 +67,7 @@ check_called_just_in_time() gint exit_code = 0; g_spawn_sync(NULL, (gchar**)argv2, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, &exit_code, NULL); /* return TRUE, if network.target is not yet active */ - return !g_spawn_check_exit_status(exit_code, NULL); + return !g_spawn_check_wait_status(exit_code, NULL); } g_free(output); return FALSE; diff --git a/src/util.c b/src/util.c index a4c0dba..5861e13 100644 --- a/src/util.c +++ b/src/util.c @@ -188,7 +188,7 @@ systemd_escape(char* string) gchar *argv[] = {"bin" "/" "systemd-escape", string, NULL}; g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &escaped, &stderrh, &exit_status, &err); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) { // LCOV_EXCL_START g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", string, exit_status, escaped, stderrh); -- cgit v1.2.3 From 55463cf4aa567ed064af11843a04be195828b42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 3 Mar 2022 09:49:45 +0100 Subject: Import netplan.io_0.103-4.debian.tar.xz [dgit import tarball netplan.io 0.103-4 netplan.io_0.103-4.debian.tar.xz] --- changelog | 158 ++++++ control | 95 ++++ copyright | 27 + gbp.conf | 4 + libnetplan-dev.dirs | 1 + libnetplan-dev.install | 2 + libnetplan0.install | 1 + libnetplan0.symbols | 51 ++ netplan.io.dirs | 2 + netplan.io.install | 4 + .../0001-parse-nm-fix-32bit-format-string.patch | 21 + patches/autopkgtest-fixes.patch | 54 ++ patches/glib-2.70-compat.patch | 87 ++++ patches/nm-1.32.10-compat.patch | 21 + patches/ovs-timeout.patch | 548 +++++++++++++++++++++ patches/series | 5 + rules | 11 + source/format | 1 + tests/autostart.sh | 79 +++ tests/cloud-init.sh | 115 +++++ tests/control | 155 ++++++ tests/prepare-testbed.sh | 16 + upstream/metadata | 7 + watch | 4 + 24 files changed, 1469 insertions(+) create mode 100644 changelog create mode 100644 control create mode 100644 copyright create mode 100644 gbp.conf create mode 100644 libnetplan-dev.dirs create mode 100644 libnetplan-dev.install create mode 100644 libnetplan0.install create mode 100644 libnetplan0.symbols create mode 100644 netplan.io.dirs create mode 100644 netplan.io.install create mode 100644 patches/0001-parse-nm-fix-32bit-format-string.patch create mode 100644 patches/autopkgtest-fixes.patch create mode 100644 patches/glib-2.70-compat.patch create mode 100644 patches/nm-1.32.10-compat.patch create mode 100644 patches/ovs-timeout.patch create mode 100644 patches/series create mode 100755 rules create mode 100644 source/format create mode 100755 tests/autostart.sh create mode 100755 tests/cloud-init.sh create mode 100644 tests/control create mode 100755 tests/prepare-testbed.sh create mode 100644 upstream/metadata create mode 100644 watch diff --git a/changelog b/changelog new file mode 100644 index 0000000..1ea81ae --- /dev/null +++ b/changelog @@ -0,0 +1,158 @@ +netplan.io (0.103-4) unstable; urgency=medium + + * Fix OVS timeouts in containers where the host is not OVS enabled + * d/t/control: Add explicit wpasupplicant test Depends + * d/t/control: mark ethernets and bonds tests as flaky + + -- Lukas Märdian Thu, 03 Mar 2022 09:49:45 +0100 + +netplan.io (0.103-3) unstable; urgency=medium + + [ Andrej Shadura ] + * Explicitly depend on glib 2.70 + + [ Lukas Märdian ] + * Fix autopkgtests inside a LXC test-runner + + d/t/prepare-testbed.sh: enable udevd (inside LXC) + + d/tests/control: enable autostart & cloud-init tests in LXC + + d/tests/control: mark scenarios test as flaky + + d/tests/control: add breaks-testbed restriction + + -- Lukas Märdian Fri, 22 Oct 2021 09:22:22 +0200 + +netplan.io (0.103-2) unstable; urgency=medium + + * Allow build-depending on openvswitch on all architectures. + Now that #979366 has been fixed, it should not be an issue anymore. + + -- Andrej Shadura Thu, 21 Oct 2021 11:19:25 +0200 + +netplan.io (0.103-1) unstable; urgency=medium + + * New upstream release: 0.103 (LP: #1938920). + - Add YAML generator and Keyfile parser for NetworkManager YAML backend + - Add activation-mode parameter, needs systemd v248+ (LP: #1664844) + - Make use of systemd-networkd's reload/reconfigure commands + - Deprecate gateway4 & gateway6 in favor of default routes (LP: #1756590) + - Add io.netplan.Netplan.Generate() DBus method + - Changed the way of how unmanaged-devices are handled by NetworkManager + - Improve integration test suite (LP: #1922126) + * Update build-dep to fix FTCBFS (Closes: #961466). + * Bump systemd dependency to >= v248 for the activation-mode feature. + * Run some autopkgtests with Restriction: isolation-container. + * Bump Standards-Version to 4.6.0.1, no changes needed. + * Update debian/watch + * Update debian/upstream/metadata + * d/control: Add Rules-Requires-Root: no + + -- Lukas Märdian Wed, 20 Oct 2021 13:22:07 +0200 + +netplan.io (0.101-4) unstable; urgency=medium + + * Build-depend on ovs on amd64 only due to a bug in its postinst. + See #979366 for details. + * Drop the custom build profile, nocheck is enough. + + -- Andrej Shadura Tue, 05 Jan 2021 22:01:50 +0100 + +netplan.io (0.101-3) unstable; urgency=medium + + * Mark the package linux-any. + * Skip openvswitch-switch dependency on m68k and ppc64. + + -- Andrej Shadura Tue, 05 Jan 2021 19:28:50 +0100 + +netplan.io (0.101-2) unstable; urgency=medium + + * Reindent debian/control. + * Add build profiles. + * Add cloud tests but mark them as flaky and skip-not-installable + for now. + + -- Andrej Shadura Tue, 05 Jan 2021 17:40:42 +0100 + +netplan.io (0.101-1) unstable; urgency=medium + + [ Andrej Shadura ] + * New upstream release. + * Merge changes from Ubuntu. + * Let tests fail. + * Remove the hack to fix build with GCC 10 (actually closes: #957603). + + [ Lukas Märdian ] + * d/control: fix lintian warning about trailing whitespace + * d/p/0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch: + Fix MAC address changes with systemd v247 by using a new approach inside + systemd's .network file. It also works with older version of systemd. + * Add d/p/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch: + Allows parsing of networkmanager: backend handlers for modem devices + * Update symbols file + + [ Michael Biebl ] + * Stop using deprecated systemd-resolve tool (Closes: #979266). + + -- Andrej Shadura Mon, 04 Jan 2021 20:34:58 +0100 + +netplan.io (0.99-2) experimental; urgency=medium + + * Split libnetplan off into separate packages. + * Force -fcommon to enable builds with GCC 10 to work around #957603. + + -- Andrej Shadura Mon, 27 Apr 2020 17:17:54 +0200 + +netplan.io (0.99-1) unstable; urgency=medium + + [ Andrej Shadura ] + * New upstream release. + * Drop old upstream patches. + * Update the co-maintainer list. + * Bump Standards-Version to 4.5.0. + * Update copyright years. + + [ Lukas Märdian ] + * debian:tests:control: add autopkgtest dependencies. + + -- Andrej Shadura Mon, 27 Apr 2020 11:01:26 +0200 + +netplan.io (0.98-2) unstable; urgency=medium + + * Cherry-pick upstream commits. + * Use debhelper-compat instead of debian/compat. + * Bump debhelper from old 11 to 12. + * Bump Standards-Version to 4.4.1 (no changes). + + -- Andrej Shadura Fri, 01 Nov 2019 15:21:21 +0100 + +netplan.io (0.98-1) unstable; urgency=medium + + [ Andrej Shadura ] + * New upstream release: 0.98 (LP: #1840832). + * Run all autopkgtests with Restriction: isolation-machine (Closes: + #919426). + + [ Mathieu Trudel-Lapierre ] + * debian/control: Add Build-Depends on libsystemd-dev for the D-Bus feature, + and on dbus-x11 for dbus-launch used in tests. + + -- Andrej Shadura Thu, 26 Sep 2019 14:35:32 +0200 + +netplan.io (0.95-2) unstable; urgency=medium + + * Set Priority to optional (Closes: #920327). + + -- Andrej Shadura Thu, 24 Jan 2019 09:43:13 +0100 + +netplan.io (0.95-1) unstable; urgency=medium + + * New upstream release. + * Update autopkgtests from the upstream. + * Add debian/watch following GitHub releases. + * Add Homepage (Closes: #917233). + + -- Andrej Shadura Sat, 29 Dec 2018 16:34:23 +0100 + +netplan.io (0.40.2-1) unstable; urgency=medium + + * Upload to Debian (Closes: #882661). + + -- Andrej Shadura Wed, 14 Nov 2018 16:29:42 -0800 diff --git a/control b/control new file mode 100644 index 0000000..e6437ad --- /dev/null +++ b/control @@ -0,0 +1,95 @@ +Source: netplan.io +Maintainer: Debian netplan Maintainers +Uploaders: + Andrej Shadura , + Mathieu Trudel-Lapierre , + Łukasz 'sil2100' Zemczak , + Lukas Märdian +Section: net +Priority: optional +Standards-Version: 4.6.0.1 +Rules-Requires-Root: no +Build-Depends: + debhelper-compat (= 13), + pkg-config, + bash-completion, + libyaml-dev, + libglib2.0-dev (>= 2.70.0~), + uuid-dev, + python3 (>= 3.1), + python3-coverage , + python3-yaml , + python3-netifaces , + libsystemd-dev, + systemd, + dbus-x11 , + pyflakes3 , + pycodestyle | pep8 , + python3-nose , + pandoc, + openvswitch-switch , +Vcs-Git: https://salsa.debian.org/debian/netplan.io.git +Vcs-Browser: https://salsa.debian.org/debian/netplan.io +Homepage: https://netplan.io/ + +Package: netplan.io +Architecture: linux-any +Multi-Arch: foreign +Depends: + ${shlibs:Depends}, + ${misc:Depends}, + iproute2, + libnetplan0 (>= ${binary:Version}), + python3, + python3-yaml, + python3-netifaces, + systemd (>= 248~), +Suggests: + network-manager | wpasupplicant, + openvswitch-switch, +Conflicts: netplan +Breaks: nplan (<< 0.34~), network-manager (<< 1.2.2-1) +Replaces: nplan (<< 0.34~) +Provides: nplan +Description: YAML network configuration abstraction for various backends + netplan reads YAML network configuration files which are written + by administrators, installers, cloud image instantiations, or other OS + deployments. During early boot it then generates backend specific + configuration files in /run to hand off control of devices to a particular + networking daemon. + . + Currently supported backends are networkd and NetworkManager. + +Package: libnetplan0 +Architecture: linux-any +Multi-Arch: same +Depends: + ${shlibs:Depends}, + ${misc:Depends}, +Description: YAML network configuration abstraction runtime library + netplan reads YAML network configuration files which are written + by administrators, installers, cloud image instantiations, or other OS + deployments. During early boot it then generates backend specific + configuration files in /run to hand off control of devices to a particular + networking daemon. + . + Currently supported backends are networkd and NetworkManager. + . + This package contains the necessary runtime library files. + +Package: libnetplan-dev +Architecture: linux-any +Multi-Arch: same +Depends: ${misc:Depends}, + libnetplan0 (= ${binary:Version}), +Description: Development files for netplan's libnetplan runtime library + netplan reads YAML network configuration files which are written + by administrators, installers, cloud image instantiations, or other OS + deployments. During early boot it then generates backend specific + configuration files in /run to hand off control of devices to a particular + networking daemon. + . + Currently supported backends are networkd and NetworkManager. + . + This package contains development files for developers wanting to use + libnetplan in their applications. diff --git a/copyright b/copyright new file mode 100644 index 0000000..f99d13e --- /dev/null +++ b/copyright @@ -0,0 +1,27 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: netplan.io +Upstream-Contact: Łukasz 'sil2100' Zemczak +Source: https://github.com/canonical/netplan + +Files: * +Copyright: 2016—2020 Canonical Ltd. +License: GPL-3 + +Files: debian/* +Copyright: + 2016—2020 Canonical Ltd. + 2018—2020 Andrej Shadura +License: GPL-3 + +License: GPL-3 + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; version 3. + . + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + On Debian systems, the complete text of the GNU Lesser General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/gbp.conf b/gbp.conf new file mode 100644 index 0000000..55e760f --- /dev/null +++ b/gbp.conf @@ -0,0 +1,4 @@ +[DEFAULT] +debian-branch=debian/unstable +upstream-branch=upstream/latest +upstream-vcs-tag=%(version)s diff --git a/libnetplan-dev.dirs b/libnetplan-dev.dirs new file mode 100644 index 0000000..99379cd --- /dev/null +++ b/libnetplan-dev.dirs @@ -0,0 +1 @@ +usr/include/netplan diff --git a/libnetplan-dev.install b/libnetplan-dev.install new file mode 100644 index 0000000..911f5de --- /dev/null +++ b/libnetplan-dev.install @@ -0,0 +1,2 @@ +usr/include/netplan/*.h +usr/lib/*/libnetplan*.so diff --git a/libnetplan0.install b/libnetplan0.install new file mode 100644 index 0000000..514ba93 --- /dev/null +++ b/libnetplan0.install @@ -0,0 +1 @@ +usr/lib/*/libnetplan*.so.* diff --git a/libnetplan0.symbols b/libnetplan0.symbols new file mode 100644 index 0000000..b4c073c --- /dev/null +++ b/libnetplan0.symbols @@ -0,0 +1,51 @@ +libnetplan.so.0.0 libnetplan0 #MINVER# + NETPLAN_OPTIONAL_ADDRESS_TYPES@Base 0.99 + NETPLAN_WIFI_WOWLAN_TYPES@Base 0.99 + _serialize_yaml@Base 0.103 + _write_netplan_conf@Base 0.102 + address_option_handlers@Base 0.100 + contains_netdef_type@Base 0.103 + cur_filename@Base 0.102 + current_file@Base 0.99 + find_yaml_glob@Base 0.101 + g_string_free_to_file@Base 0.99 + get_global_network@Base 0.103 + is_hostname@Base 0.100 + is_ip4_address@Base 0.99 + is_ip6_address@Base 0.99 + is_wireguard_key@Base 0.100 + missing_id@Base 0.99 + missing_ids_found@Base 0.99 + netdefs@Base 0.99 + netdefs_ordered@Base 0.99 + netplan_clear_netdefs@Base 0.101 + netplan_delete_connection@Base 0.102 + netplan_finish_parse@Base 0.99 + netplan_generate@Base 0.102 + netplan_get_filename_by_id@Base 0.102 + netplan_get_global_backend@Base 0.99 + netplan_get_id_from_nm_filename@Base 0.102 + netplan_netdef_new@Base 0.102 + netplan_parse_keyfile@Base 0.102 + netplan_parse_yaml@Base 0.99 + ovs_settings_global@Base 0.100 + parser_error@Base 0.99 + process_input_file@Base 0.102 + process_yaml_hierarchy@Base 0.102 + safe_mkdir_p_dir@Base 0.99 + systemd_escape@Base 0.100 + tmp@Base 0.103 + tunnel_mode_to_string@Base 0.99 + unlink_glob@Base 0.99 + validate_backend_rules@Base 0.99 + validate_default_route_consistency@Base 0.103 + validate_netdef_grammar@Base 0.99 + validate_ovs_target@Base 0.100 + wifi_frequency_24@Base 0.99 + wifi_frequency_5@Base 0.99 + wifi_get_freq24@Base 0.99 + wifi_get_freq5@Base 0.99 + wireguard_peer_handlers@Base 0.100 + write_netplan_conf@Base 0.102 + write_netplan_conf_full@Base 0.103 + yaml_error@Base 0.99 diff --git a/netplan.io.dirs b/netplan.io.dirs new file mode 100644 index 0000000..3cc4699 --- /dev/null +++ b/netplan.io.dirs @@ -0,0 +1,2 @@ +etc/netplan +lib/netplan diff --git a/netplan.io.install b/netplan.io.install new file mode 100644 index 0000000..88c78f1 --- /dev/null +++ b/netplan.io.install @@ -0,0 +1,4 @@ +lib/netplan/* +lib/systemd/system-generators/netplan +usr/share/* +usr/sbin/netplan diff --git a/patches/0001-parse-nm-fix-32bit-format-string.patch b/patches/0001-parse-nm-fix-32bit-format-string.patch new file mode 100644 index 0000000..3f22152 --- /dev/null +++ b/patches/0001-parse-nm-fix-32bit-format-string.patch @@ -0,0 +1,21 @@ +From: =?utf-8?q?Lukas_M=C3=A4rdian?= +Date: Wed, 4 Aug 2021 15:15:45 +0200 +Subject: parse-nm: fix 32bit format string + +--- + src/parse-nm.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/parse-nm.c b/src/parse-nm.c +index 9b09e34..532d17a 100644 +--- a/src/parse-nm.c ++++ b/src/parse-nm.c +@@ -136,7 +136,7 @@ static void + handle_bridge_uint(GKeyFile* kf, const gchar* key, NetplanNetDefinition* nd, char** dataptr) { + if (g_key_file_get_uint64(kf, "bridge", key, NULL)) { + nd->custom_bridging = TRUE; +- *dataptr = g_strdup_printf("%lu", g_key_file_get_uint64(kf, "bridge", key, NULL)); ++ *dataptr = g_strdup_printf("%"G_GUINT64_FORMAT, g_key_file_get_uint64(kf, "bridge", key, NULL)); + _kf_clear_key(kf, "bridge", key); + } + } diff --git a/patches/autopkgtest-fixes.patch b/patches/autopkgtest-fixes.patch new file mode 100644 index 0000000..328dbad --- /dev/null +++ b/patches/autopkgtest-fixes.patch @@ -0,0 +1,54 @@ +diff --git a/tests/integration/base.py b/tests/integration/base.py +index 5042bf4..93ee722 100644 +--- a/tests/integration/base.py ++++ b/tests/integration/base.py +@@ -75,7 +75,7 @@ class IntegrationTestsBase(unittest.TestCase): + + os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) + with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: +- f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') ++ f.write('[keyfile]\nunmanaged-devices+=interface-name:en*,eth0,veth42,veth43,nptestsrv') + subprocess.check_call(['netplan', 'apply']) + subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) + +@@ -144,12 +144,6 @@ class IntegrationTestsBase(unittest.TestCase): + universal_newlines=True) + klass.dev_e2_client_mac = out.split()[2] + +- os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) +- +- # work around https://launchpad.net/bugs/1615044 +- with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: +- f.write('[keyfile]\nunmanaged-devices=') +- + @classmethod + def shutdown_devices(klass): + '''Remove test devices''' +@@ -432,9 +426,10 @@ class IntegrationTestsWifi(IntegrationTestsBase): + klass.dev_w_ap = devs[0] + klass.dev_w_client = devs[1] + ++ os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) + # don't let NM trample over our fake AP + with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: +- f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) ++ f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=%s\n' % klass.dev_w_ap) + + @classmethod + def shutdown_devices(klass): +diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py +index ce016da..865c0d4 100644 +--- a/tests/integration/ethernets.py ++++ b/tests/integration/ethernets.py +@@ -252,9 +252,8 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests): + %(ec)s: + dhcp6: no + accept-ra: yes +- addresses: [ '192.168.1.100/24' ] +- %(e2c)s: {}''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) +- self.generate_and_settle() ++ addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) ++ self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], []) + + def test_eth_dhcp6_off_no_accept_ra(self): diff --git a/patches/glib-2.70-compat.patch b/patches/glib-2.70-compat.patch new file mode 100644 index 0000000..05d23f6 --- /dev/null +++ b/patches/glib-2.70-compat.patch @@ -0,0 +1,87 @@ +From: =?utf-8?q?Lukas_M=C3=A4rdian?= +Date: Fri, 1 Oct 2021 12:39:40 +0200 +Subject: generate:dbus:util: glib 2.70 compat + +g_spawn_check_exit_status is deprecated since libglib 2.70 and replaced +by g_spawn_check_wait_status +--- + src/dbus.c | 10 +++++----- + src/generate.c | 2 +- + src/util.c | 2 +- + 3 files changed, 7 insertions(+), 7 deletions(-) + +diff --git a/src/dbus.c b/src/dbus.c +index f0aa53a..74032dc 100644 +--- a/src/dbus.c ++++ b/src/dbus.c +@@ -110,7 +110,7 @@ _try_accept(bool accept, sd_bus_message *m, NetplanData *d, sd_bus_error *ret_er + * Check return code/errors. */ + kill(d->try_pid, signal); + waitpid(d->try_pid, &status, 0); +- g_spawn_check_exit_status(status, &error); ++ g_spawn_check_wait_status(status, &error); + if (error != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan try failed: %s", error->message); // LCOV_EXCL_LINE + +@@ -228,7 +228,7 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "cannot run netplan apply: %s", err->message); +- g_spawn_check_exit_status(exit_status, &err); ++ g_spawn_check_wait_status(exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "netplan apply failed: %s\nstdout: '%s'\nstderr: '%s'", +@@ -257,7 +257,7 @@ method_generate(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "cannot run netplan generate: %s", err->message); +- g_spawn_check_exit_status(exit_status, &err); ++ g_spawn_check_wait_status(exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, + "netplan generate failed: %s\nstdout: '%s'\nstderr: '%s'", +@@ -334,7 +334,7 @@ method_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan get: %s", err->message); // LCOV_EXCL_LINE + +- g_spawn_check_exit_status(exit_status, &err); ++ g_spawn_check_wait_status(exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan get failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE + +@@ -380,7 +380,7 @@ method_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan set %s: %s", config_delta, err->message); // LCOV_EXCL_LINE + +- g_spawn_check_exit_status(exit_status, &err); ++ g_spawn_check_wait_status(exit_status, &err); + if (err != NULL) + return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan set failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE + +diff --git a/src/generate.c b/src/generate.c +index cccd47a..24e60a2 100644 +--- a/src/generate.c ++++ b/src/generate.c +@@ -67,7 +67,7 @@ check_called_just_in_time() + gint exit_code = 0; + g_spawn_sync(NULL, (gchar**)argv2, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, &exit_code, NULL); + /* return TRUE, if network.target is not yet active */ +- return !g_spawn_check_exit_status(exit_code, NULL); ++ return !g_spawn_check_wait_status(exit_code, NULL); + } + g_free(output); + return FALSE; +diff --git a/src/util.c b/src/util.c +index a4c0dba..5861e13 100644 +--- a/src/util.c ++++ b/src/util.c +@@ -188,7 +188,7 @@ systemd_escape(char* string) + + gchar *argv[] = {"bin" "/" "systemd-escape", string, NULL}; + g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &escaped, &stderrh, &exit_status, &err); +- g_spawn_check_exit_status(exit_status, &err); ++ g_spawn_check_wait_status(exit_status, &err); + if (err != NULL) { + // LCOV_EXCL_START + g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", string, exit_status, escaped, stderrh); diff --git a/patches/nm-1.32.10-compat.patch b/patches/nm-1.32.10-compat.patch new file mode 100644 index 0000000..d98f101 --- /dev/null +++ b/patches/nm-1.32.10-compat.patch @@ -0,0 +1,21 @@ +Description: Fix ethernets test with network-manage 1.32.10 + NetworkManager's udev rules are smarter about catching renamed veths now, so + the udev rule the integration tests use to manage their veths need to list the + names the veths are renamed to too. +Author: Michael Hudson-Doyle +Origin: vendor +Forwarded: no +Last-Update: 2021-09-01 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +--- a/tests/integration/base.py ++++ b/tests/integration/base.py +@@ -70,7 +70,7 @@ + # ensure NM can manage our fake eths + os.makedirs('/run/udev/rules.d', exist_ok=True) + with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: +- f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') ++ f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43|iface1|iface2", ENV{NM_UNMANAGED}="0"\n') + subprocess.check_call(['udevadm', 'control', '--reload']) + + os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) diff --git a/patches/ovs-timeout.patch b/patches/ovs-timeout.patch new file mode 100644 index 0000000..3314b84 --- /dev/null +++ b/patches/ovs-timeout.patch @@ -0,0 +1,548 @@ +Description: Time out on ovs-vsctl commands if OVS is not enabled on the host +Author: Lukas Märdian +Forwarded: https://github.com/canonical/netplan/pull/266 +Last-Update: 2022-03-03 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +--- +diff --git a/src/openvswitch.c b/src/openvswitch.c +index 2088fbe..ae6b494 100644 +--- a/src/openvswitch.c ++++ b/src/openvswitch.c +@@ -57,7 +57,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, + g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency); + } + +- g_string_append(s, "\n[Service]\nType=oneshot\n"); ++ g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n"); + g_string_append(s, cmds->str); + + g_string_free_to_file(s, rootdir, path, NULL); +diff --git a/tests/generator/base.py b/tests/generator/base.py +index d72974b..b5167bc 100644 +--- a/tests/generator/base.py ++++ b/tests/generator/base.py +@@ -66,9 +66,9 @@ standalone\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s mcast_snooping_ena + Bridge %(iface)s external-ids:netplan/mcast_snooping_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s \ + rstp_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan/rstp_enable=false\n' + OVS_BR_EMPTY = _OVS_BASE + 'After=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n\n[Service]\n\ +-Type=oneshot\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT ++Type=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT + OVS_CLEANUP = _OVS_BASE + 'ConditionFileIsExecutable=/usr/bin/ovs-vsctl\nBefore=network.target\nWants=network.target\n\n\ +-[Service]\nType=oneshot\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' ++[Service]\nType=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' + UDEV_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", ATTR{address}=="%s", NAME="%s"\n' + UDEV_NO_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", NAME="%s"\n' + UDEV_SRIOV_RULE = 'ACTION=="add", SUBSYSTEM=="net", ATTRS{sriov_totalvfs}=="?*", RUN+="/usr/sbin/netplan apply --sriov-only"\n' +diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py +index e7084a9..4c9dfbe 100644 +--- a/tests/generator/test_ovs.py ++++ b/tests/generator/test_ovs.py +@@ -50,6 +50,7 @@ class TestOpenVSwitch(TestBase): + self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0 +@@ -60,6 +61,7 @@ After=netplan-ovs-ovs0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:iface-id=myhostname + ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/external-ids/iface-id=myhostname + ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:disable-in-band=true +@@ -71,6 +73,7 @@ After=netplan-ovs-ovs0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false + ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false + '''}, +@@ -109,6 +112,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-confi + self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:iface-id=myhostname + ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/external-ids/iface-id=myhostname + ExecStart=/usr/bin/ovs-vsctl set open_vswitch . other-config:disable-in-band=true +@@ -129,6 +133,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/other-confi + self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 + ''' + OVS_BR_DEFAULT % {'iface': 'ovs0'} + '''\ + ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 protocols=OpenFlow10,OpenFlow11,OpenFlow12 +@@ -185,6 +190,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 + ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true + ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +@@ -253,6 +259,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 + ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true + ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=active +@@ -318,6 +325,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 + ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true + ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +@@ -357,6 +365,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 + ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true + ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +@@ -408,6 +417,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=activ + ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 +@@ -432,6 +442,7 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 + ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ + ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:iface-id=myhostname +@@ -462,6 +473,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/other-config/di + ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 +@@ -521,6 +533,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/rstp_enable=tru + ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 + ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ + ExecStart=/usr/bin/ovs-vsctl set Bridge br0 protocols=OpenFlow10,OpenFlow11,OpenFlow15 +@@ -570,6 +583,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/protocols=OpenF + ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 + ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ + ExecStart=/usr/bin/ovs-vsctl set-controller br0 ptcp: ptcp:1337 ptcp:1337:[fe80::1234%eth0] pssl:1337:[fe80::1] ssl:10.10.10.1 \ +@@ -583,6 +597,7 @@ ExecStart=/usr/bin/ovs-vsctl set Controller br0 external-ids:netplan/connection- + 'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path + ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path + '''}, +@@ -680,6 +695,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set- + self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path + ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path + '''}, +@@ -784,6 +800,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 + ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true + ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +@@ -832,6 +849,7 @@ Bond=bond0 + 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patchx -- set Interface patchx type=patch options:peer=patchy + ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, +@@ -841,6 +859,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 patchy eth0 -- set Interface patchy type=patch options:peer=patchx + ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true + ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off +@@ -852,6 +871,7 @@ After=netplan-ovs-br1.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set Port patchx external-ids:netplan=true + '''}, + 'patchy.service': OVS_VIRTUAL % {'iface': 'patchy', 'extra': +@@ -860,6 +880,7 @@ After=netplan-ovs-bond0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true + '''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) +@@ -887,12 +908,14 @@ ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 patch0-1 -- set Interface patch0-1 type=patch options:peer=patch1-0 + ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, + 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patch1-0 -- set Interface patch1-0 type=patch options:peer=patch0-1 + ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, +@@ -902,6 +925,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set Port patch0-1 external-ids:netplan=true + '''}, + 'patch1-0.service': OVS_VIRTUAL % {'iface': 'patch1-0', 'extra': +@@ -910,6 +934,7 @@ After=netplan-ovs-br1.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true + '''}, + 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) +@@ -934,6 +959,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true + self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 + ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, + 'br0.100.service': OVS_VIRTUAL % {'iface': 'br0.100', 'extra': +@@ -942,6 +968,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 + ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true + '''}, +@@ -971,6 +998,7 @@ After=netplan-ovs-br0.service + + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 + ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true + '''}, +@@ -1007,6 +1035,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true + self.assert_ovs({'ovs-br.service': OVS_VIRTUAL % {'iface': 'ovs-br', 'extra': ''' + [Service] + Type=oneshot ++TimeoutStartSec=10s + ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs-br + ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs-br non-ovs-bond + ''' + OVS_BR_DEFAULT % {'iface': 'ovs-br'}}, +diff --git a/tests/integration/ovs.py b/tests/integration/ovs.py +index 8a6f60d..8f9a550 100644 +--- a/tests/integration/ovs.py ++++ b/tests/integration/ovs.py +@@ -31,8 +31,8 @@ class _CommonTests(): + + def _collect_ovs_settings(self, bridge0): + d = {} +- d['show'] = subprocess.check_output(['ovs-vsctl', 'show']) +- d['ssl'] = subprocess.check_output(['ovs-vsctl', 'get-ssl']) ++ d['show'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) ++ d['ssl'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-ssl']) + # Get external-ids + for tbl in ('Open_vSwitch', 'Controller', 'Bridge', 'Port', 'Interface'): + cols = 'name,external-ids' +@@ -40,37 +40,37 @@ class _CommonTests(): + cols = 'external-ids' + elif tbl == 'Controller': + cols = '_uuid,external-ids' +- d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', ++ d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', '-d', + 'bare', '--no-headings', 'list', tbl]) + # Get other-config + for tbl in ('Open_vSwitch', 'Bridge', 'Port', 'Interface'): + cols = 'name,other-config' + if tbl == 'Open_vSwitch': + cols = 'other-config' +- d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', ++ d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', '-d', + 'bare', '--no-headings', 'list', tbl]) + # Get bond settings + for col in ('bond_mode', 'lacp'): +- d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', ++ d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', + '--no-headings', 'list', 'Port']) + # Get bridge settings +- d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-fail-mode', bridge0]) ++ d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-fail-mode', bridge0]) + for col in ('mcast_snooping_enable', 'rstp_enable', 'protocols'): +- d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', ++ d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', + '--no-headings', 'list', 'Bridge']) + # Get controller settings +- d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-controller', bridge0]) ++ d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-controller', bridge0]) + for col in ('connection_mode',): +- d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '--columns=_uuid,%s' % col, '-f', 'csv', '-d', ++ d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=_uuid,%s' % col, '-f', 'csv', '-d', + 'bare', '--no-headings', 'list', 'Controller']) + return d + + def test_cleanup_interfaces(self): + self.setup_eth(None, False) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) + with open(self.config, 'w') as f: + f.write('''network: + openvswitch: +@@ -81,7 +81,7 @@ class _CommonTests(): + ovs1: {interfaces: [patch1-0]}''') + self.generate_and_settle(['ovs0', 'ovs1']) + # Basic verification that the bridges/ports/interfaces are there in OVS +- out = subprocess.check_output(['ovs-vsctl', 'show']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + self.assertIn(b' Bridge ovs0', out) + self.assertIn(b' Port patch0-1', out) + self.assertIn(b' Interface patch0-1', out) +@@ -94,7 +94,7 @@ class _CommonTests(): + %(ec)s: {addresses: ['1.2.3.4/24']}''' % {'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + # Verify that the netplan=true tagged bridges/ports have been cleaned up +- out = subprocess.check_output(['ovs-vsctl', 'show']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + self.assertNotIn(b'Bridge ovs0', out) + self.assertNotIn(b'Port patch0-1', out) + self.assertNotIn(b'Interface patch0-1', out) +@@ -105,11 +105,11 @@ class _CommonTests(): + + def test_cleanup_patch_ports(self): + self.setup_eth(None, False) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patchy']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patchy']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: +@@ -122,7 +122,7 @@ class _CommonTests(): + ovs0: {interfaces: [patch0-1, bond0]}''' % {'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, 'ovs0']) + # Basic verification that the bridges/ports/interfaces are there in OVS +- out = subprocess.check_output(['ovs-vsctl', 'show']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + self.assertIn(b' Bridge ovs0', out) + self.assertIn(b' Port patch0-1\n Interface patch0-1\n type: patch', out) + self.assertIn(b' Port bond0', out) +@@ -141,7 +141,7 @@ class _CommonTests(): + self.generate_and_settle([self.dev_e_client, 'ovs1']) + # Verify that the netplan=true tagged patch ports have been cleaned up + # even though the containing bond0 port still exists (with new patch ports) +- out = subprocess.check_output(['ovs-vsctl', 'show']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + self.assertIn(b' Bridge ovs1', out) + self.assertIn(b' Port patchy\n Interface patchy\n type: patch', out) + self.assertIn(b' Port bond0', out) +@@ -155,9 +155,9 @@ class _CommonTests(): + + def test_bridge_vlan(self): + self.setup_eth(None, True) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-data']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-data']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) + with open(self.config, 'w') as f: + f.write('''network: + version: 2 +@@ -183,7 +183,7 @@ class _CommonTests(): + 'br-data', + 'br-eth42.100']) + # Basic verification that the interfaces/ports are set up in OVS +- out = subprocess.check_output(['ovs-vsctl', 'show']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) + self.assertIn(b''' Port %(ec)b + Interface %(ec)b''' % {b'ec': self.dev_e_client.encode()}, out) +@@ -196,16 +196,16 @@ class _CommonTests(): + ['inet 192.168.5.[0-9]+/16', 'mtu 9000']) # from DHCP + self.assert_iface('br-data', ['inet 192.168.20.1/16']) + self.assert_iface(self.dev_e_client, ['mtu 9000', 'master ovs-system']) +- self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', 'br-to-vlan', ++ self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', '-t', '5', 'br-to-vlan', + 'br-%s.100' % self.dev_e_client])) + self.assertIn(b'br-%b' % self.dev_e_client.encode(), subprocess.check_output( +- ['ovs-vsctl', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) ++ ['ovs-vsctl', '-t', '5', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) + self.assertIn(b'br-%b' % self.dev_e_client.encode(), out) + + def test_bridge_base(self): + self.setup_eth(None, False) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', 'del-ssl']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', 'del-ssl']) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: +@@ -227,7 +227,7 @@ class _CommonTests(): + ''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) + # Basic verification that the interfaces/ports are in OVS +- out = subprocess.check_output(['ovs-vsctl', 'show']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + self.assertIn(b' Bridge ovsbr', out) + self.assertIn(b' Controller "tcp:127.0.0.1"', out) + self.assertIn(b' Controller "pssl:1337:[::1]"', out) +@@ -236,15 +236,15 @@ class _CommonTests(): + self.assertIn(b' Port %(ec)b\n Interface %(ec)b' % {b'ec': self.dev_e_client.encode()}, out) + self.assertIn(b' Port %(e2c)b\n Interface %(e2c)b' % {b'e2c': self.dev_e2_client.encode()}, out) + # Verify the bridge was tagged 'netplan:true' correctly +- out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', + 'list', 'Bridge', 'ovsbr']) + self.assertIn(b'netplan=true', out) + self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) + + def test_bond_base(self): + self.setup_eth(None, False) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'mybond']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'mybond']) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: +@@ -263,13 +263,13 @@ class _CommonTests(): + interfaces: [mybond]''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) + # Basic verification that the interfaces/ports are in OVS +- out = subprocess.check_output(['ovs-vsctl', 'show']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + self.assertIn(b' Bridge ovsbr', out) + self.assertIn(b' Port mybond', out) + self.assertIn(b' Interface %b' % self.dev_e_client.encode(), out) + self.assertIn(b' Interface %b' % self.dev_e2_client.encode(), out) + # Verify the bond was tagged 'netplan:true' correctly +- out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Port']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Port']) + self.assertIn(b'mybond,netplan=true', out) + # Verify bond params + out = subprocess.check_output(['ovs-appctl', 'bond/show', 'mybond']) +@@ -282,10 +282,10 @@ class _CommonTests(): + + def test_bridge_patch_ports(self): + self.setup_eth(None) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br0']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br1']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br1']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) + with open(self.config, 'w') as f: + f.write('''network: + openvswitch: +@@ -300,7 +300,7 @@ class _CommonTests(): + interfaces: [patch1-0]''') + self.generate_and_settle(['br0', 'br1']) + # Basic verification that the interfaces/ports are set up in OVS +- out = subprocess.check_output(['ovs-vsctl', 'show']) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + self.assertIn(b' Bridge br0', out) + self.assertIn(b''' Port patch0-1 + Interface patch0-1 +@@ -316,7 +316,7 @@ class _CommonTests(): + + def test_bridge_non_ovs_bond(self): + self.setup_eth(None, False) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs-br']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs-br']) + self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'non-ovs-bond']) + with open(self.config, 'w') as f: + f.write('''network: +@@ -333,7 +333,7 @@ class _CommonTests(): + openvswitch: {}''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovs-br', 'non-ovs-bond']) + # Basic verification that the interfaces/ports are set up in OVS +- out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], universal_newlines=True) + self.assertIn(' Bridge ovs-br', out) + self.assertIn(''' Port non-ovs-bond + Interface non-ovs-bond''', out) +@@ -346,7 +346,7 @@ class _CommonTests(): + + def test_vlan_maas(self): + self.setup_eth(None, False) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', '%s.21' % self.dev_e_client], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: +@@ -379,7 +379,7 @@ class _CommonTests(): + mtu: 1500''' % {'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client, 'ovs0', 'eth42.21']) + # Basic verification that the interfaces/ports are set up in OVS +- out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) ++ out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], universal_newlines=True) + self.assertIn(' Bridge ovs0', out) + self.assertIn(''' Port %(ec)s.21 + Interface %(ec)s.21''' % {'ec': self.dev_e_client}, out) +@@ -411,9 +411,9 @@ class _CommonTests(): + + def test_settings_tag_cleanup(self): + self.setup_eth(None, False) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) +- self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) ++ self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) + with open(self.config, 'w') as f: + f.write('''network: + version: 2 diff --git a/patches/series b/patches/series new file mode 100644 index 0000000..c6a4348 --- /dev/null +++ b/patches/series @@ -0,0 +1,5 @@ +0001-parse-nm-fix-32bit-format-string.patch +autopkgtest-fixes.patch +nm-1.32.10-compat.patch +glib-2.70-compat.patch +ovs-timeout.patch diff --git a/rules b/rules new file mode 100755 index 0000000..4fd024f --- /dev/null +++ b/rules @@ -0,0 +1,11 @@ +#!/usr/bin/make -f + +include /usr/share/dpkg/architecture.mk + +%: + dh $@ + +override_dh_auto_install: + dh_auto_install -- LIBDIR=/usr/lib/${DEB_HOST_MULTIARCH} + +.PHOHY: override_dh_auto_install diff --git a/source/format b/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/tests/autostart.sh b/tests/autostart.sh new file mode 100755 index 0000000..f3aee9f --- /dev/null +++ b/tests/autostart.sh @@ -0,0 +1,79 @@ +#!/bin/sh +# +# Check that netplan and systemd/networkd will properly cooperate and run +# out generator at boot. +# +set -eu + +if [ ! -x /tmp/autopkgtest-reboot ]; then + echo "SKIP: Testbed does not support reboot" + exit 0 +fi + +trap 'rm -f /etc/netplan/00test.yaml' EXIT INT QUIT PIPE TERM + +# parameters: service expect_running +assert_is_running() { + if [ "$2" = 1 ] && ! systemctl --quiet is-active "$1"; then + echo "ERROR: expected $1 to have started, but it was not" >&2 + systemctl --no-pager status "$1" + exit 1 + elif [ "$2" = 0 ] && systemctl --quiet is-active "$1"; then + echo "ERROR: expected $1 to not have started, but it was" >&2 + systemctl --no-pager status "$1" + exit 1 + else + systemctl --no-pager status "$1" || true + fi +} + +# Always try to keep the management interface up and running +mkdir -p /etc/systemd/network +cat < /etc/systemd/network/20-mgmt.network +[Match] +Name=eth0 en* + +[Network] +DHCP=yes +EOF + +case "${AUTOPKGTEST_REBOOT_MARK:-}" in + '') + echo "INFO: Doing initial check that there is no existing netplan config." + # right after installation systemd-networkd may or may not be started + assert_is_running systemd-networkd.service status + echo "INFO: systemd-networkd is fine, rebooting..." + /tmp/autopkgtest-reboot noconfig + ;; + + noconfig) + echo "INFO: Verifying that the test bridge is not up and writing config." + if ip a show dev brtest00 2>/dev/null; then + echo "ERROR: brtest00 bridge unexpectedly exists" >&2 + exit 1 + fi + mkdir -p /etc/netplan + cat < /etc/netplan/00test.yaml +network: + version: 2 + bridges: + brtest00: + addresses: [10.42.1.1/24] +EOF + + echo "INFO: Configuration written, rebooting..." + /tmp/autopkgtest-reboot config + ;; + + config) + echo "INFO: Validate that systemd-networkd is running and our test bridge exists..." + assert_is_running systemd-networkd.service 1 + ip a show dev brtest00 + echo "OK: Test bridge is configured." + ;; + + *) + echo "INTERNAL ERROR: autopkgtest marker $AUTOPKGTEST_REBOOT_MARK unexpected" >&2 + exit 1 + ;; +esac diff --git a/tests/cloud-init.sh b/tests/cloud-init.sh new file mode 100755 index 0000000..031e894 --- /dev/null +++ b/tests/cloud-init.sh @@ -0,0 +1,115 @@ +#!/bin/sh +# +# Check that netplan, systemd and cloud-init will properly cooperate +# and run newly generated service units just-in-time. +# +set -eu + +if [ ! -x /tmp/autopkgtest-reboot ]; then + echo "SKIP: Testbed does not support reboot" + exit 0 +fi + +# parameters: service expect_running +assert_is_running() { + if [ "$2" = 1 ] && ! systemctl --quiet is-active "$1"; then + echo "ERROR: expected $1 to have started, but it was not" >&2 + systemctl --no-pager status "$1" + exit 1 + elif [ "$2" = 0 ] && systemctl --quiet is-active "$1"; then + echo "ERROR: expected $1 to not have started, but it was" >&2 + systemctl --no-pager status "$1" + exit 1 + else + systemctl --no-pager status "$1" || true + fi +} + +# Always try to keep the management interface up and running +mkdir -p /etc/systemd/network +cat < /etc/systemd/network/20-mgmt.network +[Match] +Name=eth0 en* + +[Network] +DHCP=yes +EOF + +case "${AUTOPKGTEST_REBOOT_MARK:-}" in + '') + echo "INFO: Preparing configuration" + mkdir -p /etc/netplan + # Any netplan YAML config + cat < /etc/netplan/00test.yaml +network: + version: 2 + bridges: + brtest00: + addresses: [10.42.1.1/24] +EOF + # Prepare a dummy netplan service unit, which will be moved to /run/systemd/system/ + # during early boot, as if it would have been created by 'netplan generate' + cat < /netplan-dummy.service +[Unit] +Description=Check if this dummy is properly started by systemd + +[Service] +Type=oneshot +# Keep it running, so we can verify it was properly started +RemainAfterExit=yes +ExecStart=echo "Doing nothing ..." +EOF + # A service simulating cloud-init, calling 'netplan generate' during early boot + # at the 'initialization' phase of systemd (before basic.target is reached). + cat < /etc/systemd/system/cloud-init-dummy.service +[Unit] +Description=Simulating cloud-init's 'netplan generate' call during early boot +DefaultDependencies=no +Before=basic.target +Before=network.target +After=sysinit.target + +[Install] +RequiredBy=basic.target + +[Service] +Type=oneshot +# Keep it running, so we can verify it was properly started +RemainAfterExit=yes +# Simulate creating a new service unit (i.e. netplan-wpa-*.service / netplan-ovs-*.service) +ExecStart=/bin/cp /netplan-dummy.service /run/systemd/system/ +ExecStart=/usr/sbin/netplan generate +EOF + + # right after installation systemd-networkd may or may not be started + assert_is_running systemd-networkd.service status + + systemctl disable systemd-networkd.service + systemctl enable cloud-init-dummy.service + echo "INFO: Configuration written, rebooting..." + /tmp/autopkgtest-reboot config + ;; + + config) + sleep 5 # Give some time for systemd to finish the boot transaction + echo "INFO: Validate that systemd-networkd, cloud-init-dummy.service and netplan-dummy.service are running and our test bridge exists..." + assert_is_running systemd-networkd.service 1 + assert_is_running cloud-init-dummy.service 1 + assert_is_running netplan-dummy.service 1 + ip a show dev brtest00 + echo "OK: Test bridge is configured and just-in-time services running." + + # Cleanup + systemctl enable systemd-networkd.service + systemctl disable cloud-init-dummy.service + rm /netplan-dummy.service + rm /run/systemd/system/netplan-dummy.service + rm /etc/systemd/system/cloud-init-dummy.service + rm /etc/netplan/00test.yaml + ;; + + *) + echo "INTERNAL ERROR: autopkgtest marker $AUTOPKGTEST_REBOOT_MARK unexpected" >&2 + exit 1 + ;; +esac diff --git a/tests/control b/tests/control new file mode 100644 index 0000000..1d4ca6c --- /dev/null +++ b/tests/control @@ -0,0 +1,155 @@ +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=ovs +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, + openvswitch-switch, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed, skip-not-installable, flaky +Features: test-name=ovs + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=ethernets +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed, flaky +Features: test-name=ethernets + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=bridges +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed, flaky +Features: test-name=bridges + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=bonds +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed, flaky +Features: test-name=bonds + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=routing +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed +Features: test-name=routing + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=vlans +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed, flaky +Features: test-name=vlans + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=wifi +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed, flaky +Features: test-name=wifi + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=tunnels +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, + wireguard-tools, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed +Features: test-name=tunnels + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=scenarios +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed, flaky +Features: test-name=scenarios + +Test-Command: ./debian/tests/prepare-testbed.sh && python3 tests/integration/run.py --test=regressions +Tests-Directory: tests/integration +Depends: @, + systemd, + network-manager, + hostapd, + wpasupplicant, + dnsmasq-base, + libnm0, + python3-gi, + gir1.2-nm-1.0, +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed +Features: test-name=regressions + +Test-Command: ./debian/tests/prepare-testbed.sh && ./debian/tests/autostart.sh +Depends: @, + systemd, + udev +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed +Features: test-name=autostart + +Test-Command: ./debian/tests/prepare-testbed.sh && ./debian/tests/cloud-init.sh +Depends: @, + systemd, + udev +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed, flaky +Features: test-name=cloud-init diff --git a/tests/prepare-testbed.sh b/tests/prepare-testbed.sh new file mode 100755 index 0000000..d69d866 --- /dev/null +++ b/tests/prepare-testbed.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -xe +# disable ifupdown +rm -f /etc/network/interfaces +# enable systemd-networkd +systemctl unmask systemd-networkd.service +systemctl unmask systemd-networkd.socket +systemctl unmask systemd-networkd-wait-online.service +systemctl enable --now systemd-networkd.service +# enable systemd-resolved +systemctl unmask systemd-resolved.service +systemctl enable --now systemd-resolved.service +# enable systemd-udevd +mount -o remount,rw /sys +systemctl unmask systemd-udevd.service +systemctl start systemd-udevd.service diff --git a/upstream/metadata b/upstream/metadata new file mode 100644 index 0000000..896543b --- /dev/null +++ b/upstream/metadata @@ -0,0 +1,7 @@ +--- +Bug-Database: https://bugs.launchpad.net/netplan +Bug-Submit: https://bugs.launchpad.net/netplan/+filebug +Changelog: https://github.com/canonical/netplan/commits/master +Other-References: https://netplan.io +Repository: https://github.com/canonical/netplan.git +Repository-Browse: https://github.com/canonical/netplan diff --git a/watch b/watch new file mode 100644 index 0000000..b9ed2d5 --- /dev/null +++ b/watch @@ -0,0 +1,4 @@ +version=4 + +opts=filenamemangle=s/.+\/v?@ANY_VERSION@@ARCHIVE_EXT@/@PACKAGE@-$1.tar.gz/,uversionmangle=s/-?rc/~rc/ \ + https://github.com/canonical/netplan/releases .*/v?@ANY_VERSION@@ARCHIVE_EXT@ -- cgit v1.2.3 From 298af15cdbb0dfa5361e46f062766cb5ed3ef0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 4 Aug 2021 15:15:45 +0200 Subject: parse-nm: fix 32bit format string Gbp-Pq: Name 0001-parse-nm-fix-32bit-format-string.patch --- src/parse-nm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parse-nm.c b/src/parse-nm.c index 9b09e34..bf998b7 100644 --- a/src/parse-nm.c +++ b/src/parse-nm.c @@ -136,7 +136,7 @@ static void handle_bridge_uint(GKeyFile* kf, const gchar* key, NetplanNetDefinition* nd, char** dataptr) { if (g_key_file_get_uint64(kf, "bridge", key, NULL)) { nd->custom_bridging = TRUE; - *dataptr = g_strdup_printf("%lu", g_key_file_get_uint64(kf, "bridge", key, NULL)); + *dataptr = g_strdup_printf("%"G_GUINT64_FORMAT, g_key_file_get_uint64(kf, "bridge", key, NULL)); _kf_clear_key(kf, "bridge", key); } } -- cgit v1.2.3 From ab0d9543d9413157635f875a3c2b61b87c49e222 Mon Sep 17 00:00:00 2001 From: Debian netplan Maintainers Date: Thu, 3 Mar 2022 09:49:45 +0100 Subject: autopkgtest-fixes Gbp-Pq: Name autopkgtest-fixes.patch --- tests/integration/base.py | 11 +++-------- tests/integration/ethernets.py | 5 ++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index 5042bf4..93ee722 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -75,7 +75,7 @@ class IntegrationTestsBase(unittest.TestCase): os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') + f.write('[keyfile]\nunmanaged-devices+=interface-name:en*,eth0,veth42,veth43,nptestsrv') subprocess.check_call(['netplan', 'apply']) subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) @@ -144,12 +144,6 @@ class IntegrationTestsBase(unittest.TestCase): universal_newlines=True) klass.dev_e2_client_mac = out.split()[2] - os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) - - # work around https://launchpad.net/bugs/1615044 - with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices=') - @classmethod def shutdown_devices(klass): '''Remove test devices''' @@ -432,9 +426,10 @@ class IntegrationTestsWifi(IntegrationTestsBase): klass.dev_w_ap = devs[0] klass.dev_w_client = devs[1] + os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) # don't let NM trample over our fake AP with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: - f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=%s\n' % klass.dev_w_ap) @classmethod def shutdown_devices(klass): diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index ce016da..865c0d4 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -252,9 +252,8 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests): %(ec)s: dhcp6: no accept-ra: yes - addresses: [ '192.168.1.100/24' ] - %(e2c)s: {}''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.generate_and_settle() + addresses: [ '192.168.1.100/24' ]''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) self.assert_iface_up(self.dev_e_client, ['inet6 2600:'], []) def test_eth_dhcp6_off_no_accept_ra(self): -- cgit v1.2.3 From c31c8cbf74b002f18228286bd4e4b20bc675e2ea Mon Sep 17 00:00:00 2001 From: Michael Hudson-Doyle Date: Thu, 3 Mar 2022 09:49:45 +0100 Subject: Fix ethernets test with network-manage 1.32.10 Origin: vendor Forwarded: no Last-Update: 2021-09-01 NetworkManager's udev rules are smarter about catching renamed veths now, so the udev rule the integration tests use to manage their veths need to list the names the veths are renamed to too. Last-Update: 2021-09-01 Gbp-Pq: Name nm-1.32.10-compat.patch --- tests/integration/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index 93ee722..1054059 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -70,7 +70,7 @@ class IntegrationTestsBase(unittest.TestCase): # ensure NM can manage our fake eths os.makedirs('/run/udev/rules.d', exist_ok=True) with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: - f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') + f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43|iface1|iface2", ENV{NM_UNMANAGED}="0"\n') subprocess.check_call(['udevadm', 'control', '--reload']) os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) -- cgit v1.2.3 From 5d7111f47065e55e4d30a6626ba5c82651521516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Fri, 1 Oct 2021 12:39:40 +0200 Subject: generate:dbus:util: glib 2.70 compat g_spawn_check_exit_status is deprecated since libglib 2.70 and replaced by g_spawn_check_wait_status Gbp-Pq: Name glib-2.70-compat.patch --- src/dbus.c | 10 +++++----- src/generate.c | 2 +- src/util.c | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dbus.c b/src/dbus.c index f0aa53a..74032dc 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -110,7 +110,7 @@ _try_accept(bool accept, sd_bus_message *m, NetplanData *d, sd_bus_error *ret_er * Check return code/errors. */ kill(d->try_pid, signal); waitpid(d->try_pid, &status, 0); - g_spawn_check_exit_status(status, &error); + g_spawn_check_wait_status(status, &error); if (error != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan try failed: %s", error->message); // LCOV_EXCL_LINE @@ -228,7 +228,7 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan apply: %s", err->message); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan apply failed: %s\nstdout: '%s'\nstderr: '%s'", @@ -257,7 +257,7 @@ method_generate(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan generate: %s", err->message); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan generate failed: %s\nstdout: '%s'\nstderr: '%s'", @@ -334,7 +334,7 @@ method_get(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan get: %s", err->message); // LCOV_EXCL_LINE - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan get failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE @@ -380,7 +380,7 @@ method_set(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot run netplan set %s: %s", config_delta, err->message); // LCOV_EXCL_LINE - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "netplan set failed: %s\nstdout: '%s'\nstderr: '%s'", err->message, stdout, stderr); // LCOV_EXCL_LINE diff --git a/src/generate.c b/src/generate.c index cccd47a..24e60a2 100644 --- a/src/generate.c +++ b/src/generate.c @@ -67,7 +67,7 @@ check_called_just_in_time() gint exit_code = 0; g_spawn_sync(NULL, (gchar**)argv2, NULL, G_SPAWN_STDERR_TO_DEV_NULL, NULL, NULL, NULL, NULL, &exit_code, NULL); /* return TRUE, if network.target is not yet active */ - return !g_spawn_check_exit_status(exit_code, NULL); + return !g_spawn_check_wait_status(exit_code, NULL); } g_free(output); return FALSE; diff --git a/src/util.c b/src/util.c index a4c0dba..5861e13 100644 --- a/src/util.c +++ b/src/util.c @@ -188,7 +188,7 @@ systemd_escape(char* string) gchar *argv[] = {"bin" "/" "systemd-escape", string, NULL}; g_spawn_sync("/", argv, NULL, 0, NULL, NULL, &escaped, &stderrh, &exit_status, &err); - g_spawn_check_exit_status(exit_status, &err); + g_spawn_check_wait_status(exit_status, &err); if (err != NULL) { // LCOV_EXCL_START g_fprintf(stderr, "failed to ask systemd to escape %s; exit %d\nstdout: '%s'\nstderr: '%s'", string, exit_status, escaped, stderrh); -- cgit v1.2.3 From 41686255d166880c972b4ed2112a665531946d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 3 Mar 2022 09:49:45 +0100 Subject: Time out on ovs-vsctl commands if OVS is not enabled on the host Forwarded: https://github.com/canonical/netplan/pull/266 Last-Update: 2022-03-03 Last-Update: 2022-03-03 Gbp-Pq: Name ovs-timeout.patch --- src/openvswitch.c | 2 +- tests/generator/base.py | 4 +- tests/generator/test_ovs.py | 29 ++++++++++++++ tests/integration/ovs.py | 96 ++++++++++++++++++++++----------------------- 4 files changed, 80 insertions(+), 51 deletions(-) diff --git a/src/openvswitch.c b/src/openvswitch.c index 2088fbe..ae6b494 100644 --- a/src/openvswitch.c +++ b/src/openvswitch.c @@ -57,7 +57,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency); } - g_string_append(s, "\n[Service]\nType=oneshot\n"); + g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n"); g_string_append(s, cmds->str); g_string_free_to_file(s, rootdir, path, NULL); diff --git a/tests/generator/base.py b/tests/generator/base.py index d72974b..b5167bc 100644 --- a/tests/generator/base.py +++ b/tests/generator/base.py @@ -66,9 +66,9 @@ standalone\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s mcast_snooping_ena Bridge %(iface)s external-ids:netplan/mcast_snooping_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s \ rstp_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan/rstp_enable=false\n' OVS_BR_EMPTY = _OVS_BASE + 'After=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n\n[Service]\n\ -Type=oneshot\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT +Type=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT OVS_CLEANUP = _OVS_BASE + 'ConditionFileIsExecutable=/usr/bin/ovs-vsctl\nBefore=network.target\nWants=network.target\n\n\ -[Service]\nType=oneshot\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' +[Service]\nType=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' UDEV_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", ATTR{address}=="%s", NAME="%s"\n' UDEV_NO_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", NAME="%s"\n' UDEV_SRIOV_RULE = 'ACTION=="add", SUBSYSTEM=="net", ATTRS{sriov_totalvfs}=="?*", RUN+="/usr/sbin/netplan apply --sriov-only"\n' diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py index e7084a9..4c9dfbe 100644 --- a/tests/generator/test_ovs.py +++ b/tests/generator/test_ovs.py @@ -50,6 +50,7 @@ class TestOpenVSwitch(TestBase): self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0 @@ -60,6 +61,7 @@ After=netplan-ovs-ovs0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:disable-in-band=true @@ -71,6 +73,7 @@ After=netplan-ovs-ovs0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false '''}, @@ -109,6 +112,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-confi self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set open_vswitch . other-config:disable-in-band=true @@ -129,6 +133,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/other-confi self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 ''' + OVS_BR_DEFAULT % {'iface': 'ovs0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 protocols=OpenFlow10,OpenFlow11,OpenFlow12 @@ -185,6 +190,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -253,6 +259,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=active @@ -318,6 +325,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -357,6 +365,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -408,6 +417,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=activ ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 @@ -432,6 +442,7 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:iface-id=myhostname @@ -462,6 +473,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/other-config/di ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 @@ -521,6 +533,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/rstp_enable=tru ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 protocols=OpenFlow10,OpenFlow11,OpenFlow15 @@ -570,6 +583,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/protocols=OpenF ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set-controller br0 ptcp: ptcp:1337 ptcp:1337:[fe80::1234%eth0] pssl:1337:[fe80::1] ssl:10.10.10.1 \ @@ -583,6 +597,7 @@ ExecStart=/usr/bin/ovs-vsctl set Controller br0 external-ids:netplan/connection- 'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path '''}, @@ -680,6 +695,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set- self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path '''}, @@ -784,6 +800,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -832,6 +849,7 @@ Bond=bond0 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patchx -- set Interface patchx type=patch options:peer=patchy ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, @@ -841,6 +859,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 patchy eth0 -- set Interface patchy type=patch options:peer=patchx ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -852,6 +871,7 @@ After=netplan-ovs-br1.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patchx external-ids:netplan=true '''}, 'patchy.service': OVS_VIRTUAL % {'iface': 'patchy', 'extra': @@ -860,6 +880,7 @@ After=netplan-ovs-bond0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) @@ -887,12 +908,14 @@ ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 patch0-1 -- set Interface patch0-1 type=patch options:peer=patch1-0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patch1-0 -- set Interface patch1-0 type=patch options:peer=patch0-1 ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, @@ -902,6 +925,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patch0-1 external-ids:netplan=true '''}, 'patch1-0.service': OVS_VIRTUAL % {'iface': 'patch1-0', 'extra': @@ -910,6 +934,7 @@ After=netplan-ovs-br1.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) @@ -934,6 +959,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'br0.100.service': OVS_VIRTUAL % {'iface': 'br0.100', 'extra': @@ -942,6 +968,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true '''}, @@ -971,6 +998,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true '''}, @@ -1007,6 +1035,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true self.assert_ovs({'ovs-br.service': OVS_VIRTUAL % {'iface': 'ovs-br', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs-br ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs-br non-ovs-bond ''' + OVS_BR_DEFAULT % {'iface': 'ovs-br'}}, diff --git a/tests/integration/ovs.py b/tests/integration/ovs.py index 8a6f60d..8f9a550 100644 --- a/tests/integration/ovs.py +++ b/tests/integration/ovs.py @@ -31,8 +31,8 @@ class _CommonTests(): def _collect_ovs_settings(self, bridge0): d = {} - d['show'] = subprocess.check_output(['ovs-vsctl', 'show']) - d['ssl'] = subprocess.check_output(['ovs-vsctl', 'get-ssl']) + d['show'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + d['ssl'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-ssl']) # Get external-ids for tbl in ('Open_vSwitch', 'Controller', 'Bridge', 'Port', 'Interface'): cols = 'name,external-ids' @@ -40,37 +40,37 @@ class _CommonTests(): cols = 'external-ids' elif tbl == 'Controller': cols = '_uuid,external-ids' - d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', + d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', tbl]) # Get other-config for tbl in ('Open_vSwitch', 'Bridge', 'Port', 'Interface'): cols = 'name,other-config' if tbl == 'Open_vSwitch': cols = 'other-config' - d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', + d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', tbl]) # Get bond settings for col in ('bond_mode', 'lacp'): - d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', + d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', 'Port']) # Get bridge settings - d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-fail-mode', bridge0]) + d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-fail-mode', bridge0]) for col in ('mcast_snooping_enable', 'rstp_enable', 'protocols'): - d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', + d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', 'Bridge']) # Get controller settings - d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-controller', bridge0]) + d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-controller', bridge0]) for col in ('connection_mode',): - d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '--columns=_uuid,%s' % col, '-f', 'csv', '-d', + d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=_uuid,%s' % col, '-f', 'csv', '-d', 'bare', '--no-headings', 'list', 'Controller']) return d def test_cleanup_interfaces(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) with open(self.config, 'w') as f: f.write('''network: openvswitch: @@ -81,7 +81,7 @@ class _CommonTests(): ovs1: {interfaces: [patch1-0]}''') self.generate_and_settle(['ovs0', 'ovs1']) # Basic verification that the bridges/ports/interfaces are there in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs0', out) self.assertIn(b' Port patch0-1', out) self.assertIn(b' Interface patch0-1', out) @@ -94,7 +94,7 @@ class _CommonTests(): %(ec)s: {addresses: ['1.2.3.4/24']}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) # Verify that the netplan=true tagged bridges/ports have been cleaned up - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertNotIn(b'Bridge ovs0', out) self.assertNotIn(b'Port patch0-1', out) self.assertNotIn(b'Interface patch0-1', out) @@ -105,11 +105,11 @@ class _CommonTests(): def test_cleanup_patch_ports(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patchy']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patchy']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -122,7 +122,7 @@ class _CommonTests(): ovs0: {interfaces: [patch0-1, bond0]}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs0']) # Basic verification that the bridges/ports/interfaces are there in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs0', out) self.assertIn(b' Port patch0-1\n Interface patch0-1\n type: patch', out) self.assertIn(b' Port bond0', out) @@ -141,7 +141,7 @@ class _CommonTests(): self.generate_and_settle([self.dev_e_client, 'ovs1']) # Verify that the netplan=true tagged patch ports have been cleaned up # even though the containing bond0 port still exists (with new patch ports) - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs1', out) self.assertIn(b' Port patchy\n Interface patchy\n type: patch', out) self.assertIn(b' Port bond0', out) @@ -155,9 +155,9 @@ class _CommonTests(): def test_bridge_vlan(self): self.setup_eth(None, True) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-data']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-data']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) with open(self.config, 'w') as f: f.write('''network: version: 2 @@ -183,7 +183,7 @@ class _CommonTests(): 'br-data', 'br-eth42.100']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertIn(b''' Port %(ec)b Interface %(ec)b''' % {b'ec': self.dev_e_client.encode()}, out) @@ -196,16 +196,16 @@ class _CommonTests(): ['inet 192.168.5.[0-9]+/16', 'mtu 9000']) # from DHCP self.assert_iface('br-data', ['inet 192.168.20.1/16']) self.assert_iface(self.dev_e_client, ['mtu 9000', 'master ovs-system']) - self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', 'br-to-vlan', + self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', '-t', '5', 'br-to-vlan', 'br-%s.100' % self.dev_e_client])) self.assertIn(b'br-%b' % self.dev_e_client.encode(), subprocess.check_output( - ['ovs-vsctl', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) + ['ovs-vsctl', '-t', '5', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) self.assertIn(b'br-%b' % self.dev_e_client.encode(), out) def test_bridge_base(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) - self.addCleanup(subprocess.call, ['ovs-vsctl', 'del-ssl']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', 'del-ssl']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -227,7 +227,7 @@ class _CommonTests(): ''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) # Basic verification that the interfaces/ports are in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovsbr', out) self.assertIn(b' Controller "tcp:127.0.0.1"', out) self.assertIn(b' Controller "pssl:1337:[::1]"', out) @@ -236,15 +236,15 @@ class _CommonTests(): self.assertIn(b' Port %(ec)b\n Interface %(ec)b' % {b'ec': self.dev_e_client.encode()}, out) self.assertIn(b' Port %(e2c)b\n Interface %(e2c)b' % {b'e2c': self.dev_e2_client.encode()}, out) # Verify the bridge was tagged 'netplan:true' correctly - out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', + out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Bridge', 'ovsbr']) self.assertIn(b'netplan=true', out) self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) def test_bond_base(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'mybond']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'mybond']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -263,13 +263,13 @@ class _CommonTests(): interfaces: [mybond]''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) # Basic verification that the interfaces/ports are in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovsbr', out) self.assertIn(b' Port mybond', out) self.assertIn(b' Interface %b' % self.dev_e_client.encode(), out) self.assertIn(b' Interface %b' % self.dev_e2_client.encode(), out) # Verify the bond was tagged 'netplan:true' correctly - out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Port']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Port']) self.assertIn(b'mybond,netplan=true', out) # Verify bond params out = subprocess.check_output(['ovs-appctl', 'bond/show', 'mybond']) @@ -282,10 +282,10 @@ class _CommonTests(): def test_bridge_patch_ports(self): self.setup_eth(None) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) with open(self.config, 'w') as f: f.write('''network: openvswitch: @@ -300,7 +300,7 @@ class _CommonTests(): interfaces: [patch1-0]''') self.generate_and_settle(['br0', 'br1']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br0', out) self.assertIn(b''' Port patch0-1 Interface patch0-1 @@ -316,7 +316,7 @@ class _CommonTests(): def test_bridge_non_ovs_bond(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs-br']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs-br']) self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'non-ovs-bond']) with open(self.config, 'w') as f: f.write('''network: @@ -333,7 +333,7 @@ class _CommonTests(): openvswitch: {}''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovs-br', 'non-ovs-bond']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], universal_newlines=True) self.assertIn(' Bridge ovs-br', out) self.assertIn(''' Port non-ovs-bond Interface non-ovs-bond''', out) @@ -346,7 +346,7 @@ class _CommonTests(): def test_vlan_maas(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', '%s.21' % self.dev_e_client], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -379,7 +379,7 @@ class _CommonTests(): mtu: 1500''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs0', 'eth42.21']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], universal_newlines=True) self.assertIn(' Bridge ovs0', out) self.assertIn(''' Port %(ec)s.21 Interface %(ec)s.21''' % {'ec': self.dev_e_client}, out) @@ -411,9 +411,9 @@ class _CommonTests(): def test_settings_tag_cleanup(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) with open(self.config, 'w') as f: f.write('''network: version: 2 -- cgit v1.2.3 From a1f356c3a5520bdd5538e824e578db6cb752b110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 11 May 2022 18:11:20 +0200 Subject: [PATCH] Fix infinite timeouts in ovs-vsctl (Closes: #1000137) (#266) * ovs: time out OVS commands after a while * tests: time out ovs-vsctl commands ovs-vsctl will hang forever if the host is not OVS enabled, blocking the tests Gbp-Pq: Name ovs-timeout.patch --- src/openvswitch.c | 2 +- tests/generator/base.py | 4 +- tests/generator/test_ovs.py | 29 +++++++++++ tests/integration/ovs.py | 117 ++++++++++++++++++++++---------------------- 4 files changed, 91 insertions(+), 61 deletions(-) diff --git a/src/openvswitch.c b/src/openvswitch.c index 7479267..b625588 100644 --- a/src/openvswitch.c +++ b/src/openvswitch.c @@ -59,7 +59,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency); } - g_string_append(s, "\n[Service]\nType=oneshot\n"); + g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n"); g_string_append(s, cmds->str); g_string_free_to_file(s, rootdir, path, NULL); diff --git a/tests/generator/base.py b/tests/generator/base.py index d9396dd..da17167 100644 --- a/tests/generator/base.py +++ b/tests/generator/base.py @@ -66,9 +66,9 @@ standalone\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s mcast_snooping_ena Bridge %(iface)s external-ids:netplan/mcast_snooping_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s \ rstp_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan/rstp_enable=false\n' OVS_BR_EMPTY = _OVS_BASE + 'After=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n\n[Service]\n\ -Type=oneshot\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT +Type=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT OVS_CLEANUP = _OVS_BASE + 'ConditionFileIsExecutable=/usr/bin/ovs-vsctl\nBefore=network.target\nWants=network.target\n\n\ -[Service]\nType=oneshot\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' +[Service]\nType=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' UDEV_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", ATTR{address}=="%s", NAME="%s"\n' UDEV_NO_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", NAME="%s"\n' UDEV_SRIOV_RULE = 'ACTION=="add", SUBSYSTEM=="net", ATTRS{sriov_totalvfs}=="?*", RUN+="/usr/sbin/netplan apply --sriov-only"\n' diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py index dff2989..e09973f 100644 --- a/tests/generator/test_ovs.py +++ b/tests/generator/test_ovs.py @@ -50,6 +50,7 @@ class TestOpenVSwitch(TestBase): self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0 @@ -60,6 +61,7 @@ After=netplan-ovs-ovs0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:disable-in-band=true @@ -71,6 +73,7 @@ After=netplan-ovs-ovs0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false '''}, @@ -109,6 +112,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-confi self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set open_vswitch . other-config:disable-in-band=true @@ -129,6 +133,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/other-confi self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 ''' + OVS_BR_DEFAULT % {'iface': 'ovs0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 protocols=OpenFlow10,OpenFlow11,OpenFlow12 @@ -185,6 +190,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -253,6 +259,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=active @@ -318,6 +325,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -357,6 +365,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -408,6 +417,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=activ ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 @@ -432,6 +442,7 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:iface-id=myhostname @@ -462,6 +473,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/other-config/di ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 @@ -521,6 +533,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/rstp_enable=tru ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 protocols=OpenFlow10,OpenFlow11,OpenFlow15 @@ -570,6 +583,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/protocols=OpenF ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set-controller br0 ptcp: ptcp:1337 ptcp:1337:[fe80::1234%eth0] pssl:1337:[fe80::1] ssl:10.10.10.1 \ @@ -583,6 +597,7 @@ ExecStart=/usr/bin/ovs-vsctl set Controller br0 external-ids:netplan/connection- 'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path '''}, @@ -680,6 +695,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set- self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path '''}, @@ -784,6 +800,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -832,6 +849,7 @@ Bond=bond0 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patchx -- set Interface patchx type=patch options:peer=patchy ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, @@ -841,6 +859,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 patchy eth0 -- set Interface patchy type=patch options:peer=patchx ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -852,6 +871,7 @@ After=netplan-ovs-br1.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patchx external-ids:netplan=true '''}, 'patchy.service': OVS_VIRTUAL % {'iface': 'patchy', 'extra': @@ -860,6 +880,7 @@ After=netplan-ovs-bond0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) @@ -887,12 +908,14 @@ ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 patch0-1 -- set Interface patch0-1 type=patch options:peer=patch1-0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patch1-0 -- set Interface patch1-0 type=patch options:peer=patch0-1 ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, @@ -902,6 +925,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patch0-1 external-ids:netplan=true '''}, 'patch1-0.service': OVS_VIRTUAL % {'iface': 'patch1-0', 'extra': @@ -910,6 +934,7 @@ After=netplan-ovs-br1.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) @@ -934,6 +959,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'br0.100.service': OVS_VIRTUAL % {'iface': 'br0.100', 'extra': @@ -942,6 +968,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true '''}, @@ -971,6 +998,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true '''}, @@ -1007,6 +1035,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true self.assert_ovs({'ovs-br.service': OVS_VIRTUAL % {'iface': 'ovs-br', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs-br ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs-br non-ovs-bond ''' + OVS_BR_DEFAULT % {'iface': 'ovs-br'}}, diff --git a/tests/integration/ovs.py b/tests/integration/ovs.py index bfd6eb9..a7f75ef 100644 --- a/tests/integration/ovs.py +++ b/tests/integration/ovs.py @@ -31,8 +31,8 @@ class _CommonTests(): def _collect_ovs_settings(self, bridge0): d = {} - d['show'] = subprocess.check_output(['ovs-vsctl', 'show']) - d['ssl'] = subprocess.check_output(['ovs-vsctl', 'get-ssl']) + d['show'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + d['ssl'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-ssl']) # Get external-ids for tbl in ('Open_vSwitch', 'Controller', 'Bridge', 'Port', 'Interface'): cols = 'name,external-ids' @@ -40,37 +40,37 @@ class _CommonTests(): cols = 'external-ids' elif tbl == 'Controller': cols = '_uuid,external-ids' - d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', - 'bare', '--no-headings', 'list', tbl]) + d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', tbl]) # Get other-config for tbl in ('Open_vSwitch', 'Bridge', 'Port', 'Interface'): cols = 'name,other-config' if tbl == 'Open_vSwitch': cols = 'other-config' - d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', - 'bare', '--no-headings', 'list', tbl]) + d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', tbl]) # Get bond settings for col in ('bond_mode', 'lacp'): - d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', - '--no-headings', 'list', 'Port']) + d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', 'Port']) # Get bridge settings - d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-fail-mode', bridge0]) + d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-fail-mode', bridge0]) for col in ('mcast_snooping_enable', 'rstp_enable', 'protocols'): - d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', - '--no-headings', 'list', 'Bridge']) + d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', 'Bridge']) # Get controller settings - d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-controller', bridge0]) + d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-controller', bridge0]) for col in ('connection_mode',): - d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '--columns=_uuid,%s' % col, '-f', 'csv', '-d', - 'bare', '--no-headings', 'list', 'Controller']) + d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=_uuid,%s' % col, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', 'Controller']) return d def test_cleanup_interfaces(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) with open(self.config, 'w') as f: f.write('''network: openvswitch: @@ -81,7 +81,7 @@ class _CommonTests(): ovs1: {interfaces: [patch1-0]}''') self.generate_and_settle(['ovs0', 'ovs1']) # Basic verification that the bridges/ports/interfaces are there in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs0', out) self.assertIn(b' Port patch0-1', out) self.assertIn(b' Interface patch0-1', out) @@ -94,7 +94,7 @@ class _CommonTests(): %(ec)s: {addresses: ['1.2.3.4/24']}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) # Verify that the netplan=true tagged bridges/ports have been cleaned up - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertNotIn(b'Bridge ovs0', out) self.assertNotIn(b'Port patch0-1', out) self.assertNotIn(b'Interface patch0-1', out) @@ -105,11 +105,11 @@ class _CommonTests(): def test_cleanup_patch_ports(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patchy']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patchy']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -122,7 +122,7 @@ class _CommonTests(): ovs0: {interfaces: [patch0-1, bond0]}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs0']) # Basic verification that the bridges/ports/interfaces are there in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs0', out) self.assertIn(b' Port patch0-1\n Interface patch0-1\n type: patch', out) self.assertIn(b' Port bond0', out) @@ -141,7 +141,7 @@ class _CommonTests(): self.generate_and_settle([self.dev_e_client, 'ovs1']) # Verify that the netplan=true tagged patch ports have been cleaned up # even though the containing bond0 port still exists (with new patch ports) - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs1', out) self.assertIn(b' Port patchy\n Interface patchy\n type: patch', out) self.assertIn(b' Port bond0', out) @@ -155,9 +155,9 @@ class _CommonTests(): def test_bridge_vlan(self): self.setup_eth(None, True) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-data']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-data']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) with open(self.config, 'w') as f: f.write('''network: version: 2 @@ -183,7 +183,7 @@ class _CommonTests(): 'br-data', 'br-eth42.100']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertIn(b''' Port %(ec)b Interface %(ec)b''' % {b'ec': self.dev_e_client.encode()}, out) @@ -196,16 +196,16 @@ class _CommonTests(): ['inet 192.168.5.[0-9]+/16', 'mtu 9000']) # from DHCP self.assert_iface('br-data', ['inet 192.168.20.1/16']) self.assert_iface(self.dev_e_client, ['mtu 9000', 'master ovs-system']) - self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', 'br-to-vlan', + self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', '-t', '5', 'br-to-vlan', 'br-%s.100' % self.dev_e_client])) self.assertIn(b'br-%b' % self.dev_e_client.encode(), subprocess.check_output( - ['ovs-vsctl', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) + ['ovs-vsctl', '-t', '5', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) self.assertIn(b'br-%b' % self.dev_e_client.encode(), out) def test_bridge_vlan_deletion(self): self.setup_eth(None, True) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) with open(self.config, 'w') as f: f.write('''network: version: 2 @@ -228,13 +228,13 @@ class _CommonTests(): 'br-eth42.100']) # Basic verification that the underlying bridge and vlan interface are configured - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertIn(b''' Port br-%(ec)b.100 tag: 100 Interface br-%(ec)b.100 type: internal''' % {b'ec': self.dev_e_client.encode()}, out) - self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', 'br-to-vlan', + self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', '-t', '5', 'br-to-vlan', 'br-%s.100' % self.dev_e_client])) # Write a network configuration that has the .100 vlan interface removed @@ -253,14 +253,14 @@ class _CommonTests(): self.generate_and_settle([self.dev_e_client, self.state_dhcp4('br-eth42')]) # Check that the underlying bridge is still present but the vlan interface is now absent - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertNotIn(b'Port br-%(ec)b.100' % {b'ec': self.dev_e_client.encode()}, out) def test_bridge_base(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) - self.addCleanup(subprocess.call, ['ovs-vsctl', 'del-ssl']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', 'del-ssl']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -282,7 +282,7 @@ class _CommonTests(): ''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) # Basic verification that the interfaces/ports are in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovsbr', out) self.assertIn(b' Controller "tcp:127.0.0.1"', out) self.assertIn(b' Controller "pssl:1337:[::1]"', out) @@ -291,15 +291,15 @@ class _CommonTests(): self.assertIn(b' Port %(ec)b\n Interface %(ec)b' % {b'ec': self.dev_e_client.encode()}, out) self.assertIn(b' Port %(e2c)b\n Interface %(e2c)b' % {b'e2c': self.dev_e2_client.encode()}, out) # Verify the bridge was tagged 'netplan:true' correctly - out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', + out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Bridge', 'ovsbr']) self.assertIn(b'netplan=true', out) self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) def test_bond_base(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'mybond']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'mybond']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -318,13 +318,14 @@ class _CommonTests(): interfaces: [mybond]''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) # Basic verification that the interfaces/ports are in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovsbr', out) self.assertIn(b' Port mybond', out) self.assertIn(b' Interface %b' % self.dev_e_client.encode(), out) self.assertIn(b' Interface %b' % self.dev_e2_client.encode(), out) # Verify the bond was tagged 'netplan:true' correctly - out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Port']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', + '-d', 'bare', 'list', 'Port']) self.assertIn(b'mybond,netplan=true', out) # Verify bond params out = subprocess.check_output(['ovs-appctl', 'bond/show', 'mybond']) @@ -337,10 +338,10 @@ class _CommonTests(): def test_bridge_patch_ports(self): self.setup_eth(None) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) with open(self.config, 'w') as f: f.write('''network: openvswitch: @@ -355,7 +356,7 @@ class _CommonTests(): interfaces: [patch1-0]''') self.generate_and_settle(['br0', 'br1']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br0', out) self.assertIn(b''' Port patch0-1 Interface patch0-1 @@ -371,7 +372,7 @@ class _CommonTests(): def test_bridge_non_ovs_bond(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs-br']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs-br']) self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'non-ovs-bond']) with open(self.config, 'w') as f: f.write('''network: @@ -388,7 +389,7 @@ class _CommonTests(): openvswitch: {}''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovs-br', 'non-ovs-bond']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], universal_newlines=True) self.assertIn(' Bridge ovs-br', out) self.assertIn(''' Port non-ovs-bond Interface non-ovs-bond''', out) @@ -401,7 +402,7 @@ class _CommonTests(): def test_vlan_maas(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', '%s.21' % self.dev_e_client], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -434,7 +435,7 @@ class _CommonTests(): mtu: 1500''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs0', 'eth42.21']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], universal_newlines=True) self.assertIn(' Bridge ovs0', out) self.assertIn(''' Port %(ec)s.21 Interface %(ec)s.21''' % {'ec': self.dev_e_client}, out) @@ -466,9 +467,9 @@ class _CommonTests(): def test_settings_tag_cleanup(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) with open(self.config, 'w') as f: f.write('''network: version: 2 -- cgit v1.2.3 From 3aa03f4d068a58e65c381521e145d59bcd4c6215 Mon Sep 17 00:00:00 2001 From: Debian netplan Maintainers Date: Tue, 14 Jun 2022 17:39:24 +0200 Subject: autopkgtest-fixes Gbp-Pq: Name autopkgtest-fixes.patch --- tests/integration/base.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index b086385..fffcaa7 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -75,7 +75,7 @@ class IntegrationTestsBase(unittest.TestCase): os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') + f.write('[keyfile]\nunmanaged-devices+=interface-name:en*,eth0,veth42,veth43,nptestsrv') subprocess.check_call(['netplan', 'apply']) subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) @@ -144,12 +144,6 @@ class IntegrationTestsBase(unittest.TestCase): universal_newlines=True) klass.dev_e2_client_mac = out.split()[2] - os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) - - # work around https://launchpad.net/bugs/1615044 - with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices=') - @classmethod def shutdown_devices(klass): '''Remove test devices''' @@ -440,9 +434,10 @@ class IntegrationTestsWifi(IntegrationTestsBase): klass.dev_w_ap = devs[0] klass.dev_w_client = devs[1] + os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) # don't let NM trample over our fake AP with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: - f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=%s\n' % klass.dev_w_ap) @classmethod def shutdown_devices(klass): -- cgit v1.2.3 From 45a73d0baeb846f23b79150e1b4e1654338d412d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 10 Mar 2022 09:41:58 +0100 Subject: cli:apply: fix potential race with rename/creation of netdevs and start networkd if off (LP: #1962095) (#260) Calling networkctl_reload() before networkd_interfaces() makes sure that newly created netdevs will show up in the interface list. Making use of the index number (instead of interface name) ensures that renamed interfaces are properly reconfigured even if they changed their name. Starting networkd if not active is making sure we can process network changes even if systemd-networkd was stopped before (e.g. by subiquity), see LP#1962095 COMMITS: * tests:integration:base: increase debugging output * cli:apply: fix potential race with rename/creation of netdevs Calling networkctl_reload() before networkd_interfaces() makes sure that newly created netdevs will show up in the interface list. Making use of the index number (instead of interface name) ensures that renamed interfaces are properly reconfigured even if they changed their name. * tests:regressions: handle networkd inactive fallback (LP: #1962095) * cli:apply: start networkd if stopped (LP: #1962095) * tests:bonds: do not try to match on MAC while changing it at the same time When a MAC address is set for a bond, networkd will set the same MAC address to the enslaved interfaces. Therefore we cannot match on the original MAC for the ethernet device, as networkd will not find/manage that interface otherwise. https://systemd.network/systemd.netdev.html#MACAddress= Gbp-Pq: Name 0002-cli-apply-fix-potential-race-with-rename-creation-of.patch --- netplan/cli/commands/apply.py | 8 +++++++- netplan/cli/utils.py | 7 +++++-- tests/integration/base.py | 7 ++++++- tests/integration/bonds.py | 4 +--- tests/integration/regressions.py | 16 ++++++++++++++++ tests/test_utils.py | 18 +++++++++++++----- 6 files changed, 48 insertions(+), 12 deletions(-) diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index d481184..b36662a 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -252,7 +252,13 @@ class NetplanApply(utils.NetplanCommand): 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()) + try: + utils.networkctl_reload() + utils.networkctl_reconfigure(utils.networkd_interfaces()) + except subprocess.CalledProcessError: + # (re-)start systemd-networkd if it is not running, yet + logging.warning('Falling back to a hard restart of systemd-networkd.service') + utils.systemctl('restart', ['systemd-networkd.service'], sync=True) # 1st: execute OVS cleanup, to avoid races while applying OVS config utils.systemctl('start', [OVS_CLEANUP_SERVICE], sync=True) # 2nd: start all other services diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py index a05b42b..4fb1dad 100644 --- a/netplan/cli/utils.py +++ b/netplan/cli/utils.py @@ -95,12 +95,15 @@ def networkd_interfaces(): for line in out.splitlines(): s = line.strip().split(' ') if s[0].isnumeric() and s[-1] not in ['unmanaged', 'linger']: - interfaces.add(s[1]) + interfaces.add(s[0]) return interfaces -def networkctl_reconfigure(interfaces): +def networkctl_reload(): subprocess.check_call(['networkctl', 'reload']) + + +def networkctl_reconfigure(interfaces): if len(interfaces) >= 1: subprocess.check_call(['networkctl', 'reconfigure'] + list(interfaces)) diff --git a/tests/integration/base.py b/tests/integration/base.py index fffcaa7..10094dd 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -294,7 +294,12 @@ class IntegrationTestsBase(unittest.TestCase): cmd = ['netplan', 'apply'] if state_dir: cmd = cmd + ['--state', state_dir] - out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, universal_newlines=True) + out = '' + try: + out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, universal_newlines=True) + except subprocess.CalledProcessError as e: + self.assertTrue(False, 'netplan apply failed: {}'.format(e.output)) + if 'Run \'systemctl daemon-reload\' to reload units.' in out: self.fail('systemd units changed without reload') # start NM so that we can verify that it does not manage anything diff --git a/tests/integration/bonds.py b/tests/integration/bonds.py index 763f7e5..2e3773e 100644 --- a/tests/integration/bonds.py +++ b/tests/integration/bonds.py @@ -315,14 +315,12 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests): ethbn: match: name: %(ec)s - macaddress: %(ec_mac)s bonds: mybond: interfaces: [ethbn] macaddress: 00:01:02:03:04:05 dhcp4: yes''' % {'r': self.backend, - 'ec': self.dev_e_client, - 'ec_mac': self.dev_e_client_mac}) + 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24', '00:01:02:03:04:05']) diff --git a/tests/integration/regressions.py b/tests/integration/regressions.py index 40aeeac..7d8cabb 100644 --- a/tests/integration/regressions.py +++ b/tests/integration/regressions.py @@ -118,6 +118,22 @@ r'Press ENTER before the timeout to accept the new configuration\n\n\n' r'(Changes will revert in \d+ seconds\n)+' r'Reverting\.') + def test_apply_networkd_inactive_lp1962095(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: + %(ec)s: + dhcp4: true + %(e2c)s: + dhcp4: true + version: 2''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + # stop networkd to simulate the failure case + subprocess.check_call(['systemctl', 'stop', 'systemd-networkd.service', 'systemd-networkd.socket']) + self.generate_and_settle([self.state_dhcp4(self.dev_e_client), self.state_dhcp4(self.dev_e2_client)]) + self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) + self.assert_iface_up(self.dev_e2_client, ['inet 192.168.6.[0-9]+/24']) + @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") diff --git a/tests/test_utils.py b/tests/test_utils.py index b958d58..bd035a9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -215,17 +215,25 @@ class TestUtils(unittest.TestCase): 174 wwan0 wwan off linger''') res = utils.networkd_interfaces() self.assertEquals(self.mock_networkctl.calls(), [['networkctl', '--no-pager', '--no-legend']]) - self.assertIn('wlan0', res) - self.assertIn('ens3', res) + self.assertIn('2', res) + self.assertIn('3', res) + + def test_networkctl_reload(self): + self.mock_networkctl = MockCmd('networkctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env + utils.networkctl_reload() + self.assertEquals(self.mock_networkctl.calls(), [ + ['networkctl', 'reload'] + ]) def test_networkctl_reconfigure(self): self.mock_networkctl = MockCmd('networkctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env - utils.networkctl_reconfigure(['eth0', 'eth1']) + utils.networkctl_reconfigure(['3', '5']) self.assertEquals(self.mock_networkctl.calls(), [ - ['networkctl', 'reload'], - ['networkctl', 'reconfigure', 'eth0', 'eth1'] + ['networkctl', 'reconfigure', '3', '5'] ]) def test_is_nm_snap_enabled(self): -- cgit v1.2.3 From dd6f7d4fd6501f23eb606f8828c379ec6aa1a2e7 Mon Sep 17 00:00:00 2001 From: Nicolas Bock Date: Thu, 12 May 2022 01:18:36 -0600 Subject: Add tristate type for offload options (LP: #1956264) (#270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: https://bugs.launchpad.net/netplan/+bug/1956264 COMMITS: * Add tristate type for offload options * Clarify name matching issues for `networkd` * Fix tests for offload * cli: apply: properly change udev/.link offloading settings * Re-used existing offloading fields, they are ABI compatible Size: Both enum and gboolean reduce to integer, so they are of same size. Content: An old consumer looking at these will interpret UNSET as if it was TRUE, which is the kernel's default (=UNSET) value. * CI: quirk to add ethtool test dependency * tests: ethernets: link offloading validation * doc: be more specific with the offloading docs Co-authored-by: Lukas Märdian Gbp-Pq: Name 0003-Add-tristate-type-for-offload-options-LP-1956264-270.patch --- doc/netplan.md | 36 ++++++++++++------------ netplan/cli/commands/apply.py | 11 +++++++- src/netplan.c | 21 +++++++++----- src/networkd.c | 49 ++++++++++++++++++-------------- src/parse.c | 55 +++++++++++++++++++++++++++++++----- src/types.c | 8 ++++++ src/types.h | 35 ++++++++++++++++++----- tests/generator/test_ethernets.py | 34 +++++++++++++++------- tests/integration/ethernets.py | 59 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 237 insertions(+), 71 deletions(-) diff --git a/doc/netplan.md b/doc/netplan.md index e9b4e92..373e05d 100644 --- a/doc/netplan.md +++ b/doc/netplan.md @@ -79,6 +79,10 @@ Virtual devices ## Common properties for physical device types +**Note:** Some options will not work reliably for devices matched by name only +and rendered by networkd, due to interactions with device renaming in udev. +Match devices by MAC when setting options like: ``wakeonlan`` or ``*-offload``. + ``match`` (mapping) : This selects a subset of available physical devices by various hardware @@ -139,54 +143,50 @@ Virtual devices : Enable wake on LAN. Off by default. - **Note:** This will not work reliably for devices matched by name - only and rendered by networkd, due to interactions with device - renaming in udev. Match devices by MAC when setting wake on LAN. - ``emit-lldp`` (bool) – since **0.99** : (networkd backend only) Whether to emit LLDP packets. Off by default. ``receive-checksum-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the hardware offload for - checksumming of ingress network packets is enabled. When unset, +: (networkd backend only) If set to true (false), the hardware offload for + checksumming of ingress network packets is enabled (disabled). When unset, the kernel's default will be used. ``transmit-checksum-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the hardware offload for - checksumming of egress network packets is enabled. When unset, +: (networkd backend only) If set to true (false), the hardware offload for + checksumming of egress network packets is enabled (disabled). When unset, the kernel's default will be used. ``tcp-segmentation-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the TCP Segmentation - Offload (TSO) is enabled. When unset, the kernel's default will +: (networkd backend only) If set to true (false), the TCP Segmentation + Offload (TSO) is enabled (disabled). When unset, the kernel's default will be used. ``tcp6-segmentation-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the TCP6 Segmentation - Offload (tx-tcp6-segmentation) is enabled. When unset, the +: (networkd backend only) If set to true (false), the TCP6 Segmentation + Offload (tx-tcp6-segmentation) is enabled (disabled). When unset, the kernel's default will be used. ``generic-segmentation-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the Generic Segmentation - Offload (GSO) is enabled. When unset, the kernel's default will +: (networkd backend only) If set to true (false), the Generic Segmentation + Offload (GSO) is enabled (disabled). When unset, the kernel's default will be used. ``generic-receive-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the Generic Receive - Offload (GRO) is enabled. When unset, the kernel's default will +: (networkd backend only) If set to true (false), the Generic Receive + Offload (GRO) is enabled (disabled). When unset, the kernel's default will be used. ``large-receive-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the Generic Receive - Offload (GRO) is enabled. When unset, the kernel's default will +: (networkd backend only) If set to true (false), the Large Receive Offload + (LRO) is enabled (disabled). When unset, the kernel's default will be used. ``openvswitch`` (mapping) – since **0.100** diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index b36662a..9d4511f 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -221,13 +221,22 @@ class NetplanApply(utils.NetplanCommand): '/sys/class/net/' + device], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.check_call(['udevadm', 'test', + '/sys/class/net/' + device], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: logging.debug('Ignoring device without syspath: %s', device) + devices_after_udev = netifaces.interfaces() # apply some more changes manually for iface, settings in changes.items(): # rename non-critical network interfaces - if settings.get('name'): + new_name = settings.get('name') + if new_name: + if iface in devices and new_name in devices_after_udev: + logging.debug('Interface rename {} -> {} already happened.'.format(iface, new_name)) + continue # re-name already happened via 'udevadm test' # bring down the interface, using its current (matched) interface name subprocess.check_call(['ip', 'link', 'set', 'dev', iface, 'down'], stdout=subprocess.DEVNULL, diff --git a/src/netplan.c b/src/netplan.c index 7b387b4..d1a27a6 100644 --- a/src/netplan.c +++ b/src/netplan.c @@ -752,13 +752,20 @@ _serialize_yaml( YAML_BOOL_TRUE(def, event, emitter, "wakeonlan", def->wake_on_lan); /* Offload options */ - YAML_BOOL_TRUE(def, event, emitter, "receive-checksum-offload", def->receive_checksum_offload); - YAML_BOOL_TRUE(def, event, emitter, "transmit-checksum-offload", def->transmit_checksum_offload); - YAML_BOOL_TRUE(def, event, emitter, "tcp-segmentation-offload", def->tcp_segmentation_offload); - YAML_BOOL_TRUE(def, event, emitter, "tcp6-segmentation-offload", def->tcp6_segmentation_offload); - YAML_BOOL_TRUE(def, event, emitter, "generic-segmentation-offload", def->generic_segmentation_offload); - YAML_BOOL_TRUE(def, event, emitter, "generic-receive-offload", def->generic_receive_offload); - YAML_BOOL_TRUE(def, event, emitter, "large-receive-offload", def->large_receive_offload); + if (def->receive_checksum_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "receive-checksum-offload", def->receive_checksum_offload); + if (def->transmit_checksum_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "transmit-checksum-offload", def->transmit_checksum_offload); + if (def->tcp_segmentation_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "tcp-segmentation-offload", def->tcp_segmentation_offload); + if (def->tcp6_segmentation_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "tcp6-segmentation-offload", def->tcp6_segmentation_offload); + if (def->generic_segmentation_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "generic-segmentation-offload", def->generic_segmentation_offload); + if (def->generic_receive_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "generic-receive-offload", def->generic_receive_offload); + if (def->large_receive_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "large-receive-offload", def->large_receive_offload); if (def->wowlan && def->wowlan != NETPLAN_WIFI_WOWLAN_DEFAULT) { YAML_SCALAR_PLAIN(event, emitter, "wakeonwlan"); diff --git a/src/networkd.c b/src/networkd.c index 6d26047..62c87ce 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -243,13 +243,13 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char if (!def->set_name && !def->wake_on_lan && !def->mtubytes && - !def->receive_checksum_offload && - !def->transmit_checksum_offload && - !def->tcp_segmentation_offload && - !def->tcp6_segmentation_offload && - !def->generic_segmentation_offload && - !def->generic_receive_offload && - !def->large_receive_offload) + (def->receive_checksum_offload == NETPLAN_TRISTATE_UNSET) && + (def->transmit_checksum_offload == NETPLAN_TRISTATE_UNSET) && + (def->tcp_segmentation_offload == NETPLAN_TRISTATE_UNSET) && + (def->tcp6_segmentation_offload == NETPLAN_TRISTATE_UNSET) && + (def->generic_segmentation_offload == NETPLAN_TRISTATE_UNSET) && + (def->generic_receive_offload == NETPLAN_TRISTATE_UNSET) && + (def->large_receive_offload == NETPLAN_TRISTATE_UNSET)) return; /* build file contents */ @@ -265,26 +265,33 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); /* Offload options */ - if (def->receive_checksum_offload) - g_string_append_printf(s, "ReceiveChecksumOffload=%u\n", def->receive_checksum_offload); + if (def->receive_checksum_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "ReceiveChecksumOffload=%s\n", + (def->receive_checksum_offload ? "true" : "false")); - if (def->transmit_checksum_offload) - g_string_append_printf(s, "TransmitChecksumOffload=%u\n", def->transmit_checksum_offload); + if (def->transmit_checksum_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "TransmitChecksumOffload=%s\n", + (def->transmit_checksum_offload ? "true" : "false")); - if (def->tcp_segmentation_offload) - g_string_append_printf(s, "TCPSegmentationOffload=%u\n", def->tcp_segmentation_offload); + if (def->tcp_segmentation_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "TCPSegmentationOffload=%s\n", + (def->tcp_segmentation_offload ? "true" : "false")); - if (def->tcp6_segmentation_offload) - g_string_append_printf(s, "TCP6SegmentationOffload=%u\n", def->tcp6_segmentation_offload); + if (def->tcp6_segmentation_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "TCP6SegmentationOffload=%s\n", + (def->tcp6_segmentation_offload ? "true" : "false")); - if (def->generic_segmentation_offload) - g_string_append_printf(s, "GenericSegmentationOffload=%u\n", def->generic_segmentation_offload); + if (def->generic_segmentation_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "GenericSegmentationOffload=%s\n", + (def->generic_segmentation_offload ? "true" : "false")); - if (def->generic_receive_offload) - g_string_append_printf(s, "GenericReceiveOffload=%u\n", def->generic_receive_offload); + if (def->generic_receive_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "GenericReceiveOffload=%s\n", + (def->generic_receive_offload ? "true" : "false")); - if (def->large_receive_offload) - g_string_append_printf(s, "LargeReceiveOffload=%u\n", def->large_receive_offload); + if (def->large_receive_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "LargeReceiveOffload=%s\n", + (def->large_receive_offload ? "true" : "false")); orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".link"); diff --git a/src/parse.c b/src/parse.c index 350c508..0cb07d2 100644 --- a/src/parse.c +++ b/src/parse.c @@ -370,6 +370,37 @@ handle_generic_bool(NetplanParser* npp, yaml_node_t* node, void* entryptr, const return TRUE; } +/* + * Handler for setting a HashTable field from a mapping node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the boolean field to write is located + */ +static gboolean +handle_generic_tristate(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + g_assert(entryptr); + NetplanTristate v; + guint offset = GPOINTER_TO_UINT(data); + NetplanTristate* dest = ((void*) entryptr + offset); + + if (g_ascii_strcasecmp(scalar(node), "true") == 0 || + g_ascii_strcasecmp(scalar(node), "on") == 0 || + g_ascii_strcasecmp(scalar(node), "yes") == 0 || + g_ascii_strcasecmp(scalar(node), "y") == 0) + v = NETPLAN_TRISTATE_TRUE; + else if (g_ascii_strcasecmp(scalar(node), "false") == 0 || + g_ascii_strcasecmp(scalar(node), "off") == 0 || + g_ascii_strcasecmp(scalar(node), "no") == 0 || + g_ascii_strcasecmp(scalar(node), "n") == 0) + v = NETPLAN_TRISTATE_FALSE; + else + return yaml_error(npp, node, error, "invalid boolean value '%s'", scalar(node)); + + *dest = v; + mark_data_as_dirty(npp, dest); + return TRUE; +} + /* * Handler for setting a HashTable field from a mapping node, inside a given struct * @entryptr: pointer to the beginning of the to-be-modified data structure @@ -516,6 +547,16 @@ handle_netdef_bool(NetplanParser* npp, yaml_node_t* node, const void* data, GErr return handle_generic_bool(npp, node, npp->current.netdef, data, error); } +/** + * Generic handler for tri-state settings that can bei "UNSET", "TRUE", or "FALSE". + * @data: offset into NetplanNetDefinition where the guint field to write is located + */ +static gboolean +handle_netdef_tristate(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_tristate(npp, node, npp->current.netdef, data, error); +} + /** * Generic handler for setting a npp->current.netdef guint field from a scalar node * @data: offset into NetplanNetDefinition where the guint field to write is located @@ -2356,13 +2397,13 @@ static const mapping_entry_handler dhcp6_overrides_handlers[] = { {"wakeonlan", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(wake_on_lan)}, \ {"wakeonwlan", YAML_SEQUENCE_NODE, {.generic=handle_wowlan}, netdef_offset(wowlan)}, \ {"emit-lldp", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(emit_lldp)}, \ - {"receive-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(receive_checksum_offload)}, \ - {"transmit-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(transmit_checksum_offload)}, \ - {"tcp-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(tcp_segmentation_offload)}, \ - {"tcp6-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(tcp6_segmentation_offload)}, \ - {"generic-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(generic_segmentation_offload)}, \ - {"generic-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(generic_receive_offload)}, \ - {"large-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(large_receive_offload)} + {"receive-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(receive_checksum_offload)}, \ + {"transmit-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(transmit_checksum_offload)}, \ + {"tcp-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(tcp_segmentation_offload)}, \ + {"tcp6-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(tcp6_segmentation_offload)}, \ + {"generic-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(generic_segmentation_offload)}, \ + {"generic-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(generic_receive_offload)}, \ + {"large-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(large_receive_offload)} static const mapping_entry_handler ethernet_def_handlers[] = { COMMON_LINK_HANDLERS, diff --git a/src/types.c b/src/types.c index eb9f780..00c2b0f 100644 --- a/src/types.c +++ b/src/types.c @@ -335,6 +335,14 @@ reset_netdef(NetplanNetDefinition* netdef, NetplanDefType new_type, NetplanBacke reset_private_netdef_data(netdef->_private); FREE_AND_NULLIFY(netdef->_private); + + netdef->receive_checksum_offload = NETPLAN_TRISTATE_UNSET; + netdef->transmit_checksum_offload = NETPLAN_TRISTATE_UNSET; + netdef->tcp_segmentation_offload = NETPLAN_TRISTATE_UNSET; + netdef->tcp6_segmentation_offload = NETPLAN_TRISTATE_UNSET; + netdef->generic_segmentation_offload = NETPLAN_TRISTATE_UNSET; + netdef->generic_receive_offload = NETPLAN_TRISTATE_UNSET; + netdef->large_receive_offload = NETPLAN_TRISTATE_UNSET; } static void diff --git a/src/types.h b/src/types.h index 27a23fc..710b1f1 100644 --- a/src/types.h +++ b/src/types.h @@ -171,6 +171,27 @@ typedef union { } networkd; } NetplanBackendSettings; +typedef enum +{ + /** + * @brief Tristate enum type + * + * This type defines a boolean which can be unset, i.e. + * this type has three states. The enum is ordered so + * that + * + * UNSET -> -1 + * FALSE -> 0 + * TRUE -> 1 + * + * And the integer values can be used directly when + * converting to string. + */ + NETPLAN_TRISTATE_UNSET = -1, /* -1 */ + NETPLAN_TRISTATE_FALSE, /* 0 */ + NETPLAN_TRISTATE_TRUE, /* 1 */ +} NetplanTristate; + struct netplan_net_definition { NetplanDefType type; NetplanBackend backend; @@ -333,13 +354,13 @@ struct netplan_net_definition { gboolean ignore_carrier; /* offload options */ - gboolean receive_checksum_offload; - gboolean transmit_checksum_offload; - gboolean tcp_segmentation_offload; - gboolean tcp6_segmentation_offload; - gboolean generic_segmentation_offload; - gboolean generic_receive_offload; - gboolean large_receive_offload; + NetplanTristate receive_checksum_offload; + NetplanTristate transmit_checksum_offload; + NetplanTristate tcp_segmentation_offload; + NetplanTristate tcp6_segmentation_offload; + NetplanTristate generic_segmentation_offload; + NetplanTristate generic_receive_offload; + NetplanTristate large_receive_offload; struct private_netdef_data* _private; diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py index 46bf764..e81941b 100644 --- a/tests/generator/test_ethernets.py +++ b/tests/generator/test_ethernets.py @@ -772,11 +772,11 @@ method=ignore ethernets: eth1: receive-checksum-offload: true - transmit-checksum-offload: true + transmit-checksum-offload: off tcp-segmentation-offload: true - tcp6-segmentation-offload: true + tcp6-segmentation-offload: false generic-segmentation-offload: true - generic-receive-offload: true + generic-receive-offload: no large-receive-offload: true''') self.assert_networkd({'eth1.link': '''[Match] @@ -784,13 +784,13 @@ OriginalName=eth1 [Link] WakeOnLan=off -ReceiveChecksumOffload=1 -TransmitChecksumOffload=1 -TCPSegmentationOffload=1 -TCP6SegmentationOffload=1 -GenericSegmentationOffload=1 -GenericReceiveOffload=1 -LargeReceiveOffload=1 +ReceiveChecksumOffload=true +TransmitChecksumOffload=false +TCPSegmentationOffload=true +TCP6SegmentationOffload=false +GenericSegmentationOffload=true +GenericReceiveOffload=false +LargeReceiveOffload=true ''', 'eth1.network': '''[Match] Name=eth1 @@ -799,3 +799,17 @@ Name=eth1 LinkLocalAddressing=ipv6 '''}) self.assert_networkd_udev(None) + + def test_offload_invalid(self): + err = self.generate('''network: + version: 2 + ethernets: + eth1: + generic-receive-offload: n + receive-checksum-offload: true + tcp-segmentation-offload: true + tcp6-segmentation-offload: false + generic-segmentation-offload: true + transmit-checksum-offload: xx + large-receive-offload: true''', expect_fail=True) + self.assertIn('invalid boolean value \'xx\'', err) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 865c0d4..06ac069 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -236,6 +236,65 @@ class _CommonTests(): self.assert_iface_up('iface1', ['inet 10.10.10.11']) self.assert_iface_up('iface2', ['inet 10.10.10.22']) + def test_link_offloading(self): + self.setup_eth(None, False) + # check kernel defaults + out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) + self.assertIn(b'rx-checksumming: on', out) + self.assertIn(b'tx-checksumming: on', out) + self.assertIn(b'tcp-segmentation-offload: on', out) + self.assertIn(b'tx-tcp6-segmentation: on', out) + self.assertIn(b'generic-segmentation-offload: on', out) + self.assertIn(b'generic-receive-offload: off', out) # off by default + # validate turning off + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: [10.10.10.22/24] + receive-checksum-offload: off + transmit-checksum-offload: off + tcp-segmentation-offload: off + tcp6-segmentation-offload: off + generic-segmentation-offload: off + generic-receive-offload: off + #large-receive-offload: off # not possible on veth +''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 10.10.10.22']) + out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) + self.assertIn(b'rx-checksumming: off', out) + self.assertIn(b'tx-checksumming: off', out) + self.assertIn(b'tcp-segmentation-offload: off', out) + self.assertIn(b'tx-tcp6-segmentation: off', out) + self.assertIn(b'generic-segmentation-offload: off', out) + self.assertIn(b'generic-receive-offload: off', out) + # validate turning on + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: [10.10.10.22/24] + receive-checksum-offload: true + transmit-checksum-offload: true + tcp-segmentation-offload: true + tcp6-segmentation-offload: true + generic-segmentation-offload: true + generic-receive-offload: true + #large-receive-offload: true # not possible on veth +''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 10.10.10.22']) + out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) + self.assertIn(b'rx-checksumming: on', out) + self.assertIn(b'tx-checksumming: on', out) + self.assertIn(b'tcp-segmentation-offload: on', out) + self.assertIn(b'tx-tcp6-segmentation: on', out) + self.assertIn(b'generic-segmentation-offload: on', out) + self.assertIn(b'generic-receive-offload: on', out) + @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") -- cgit v1.2.3 From 399f1ca7b8c2194a0226c2b5159099452d10d524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 19 May 2022 12:43:11 +0200 Subject: tests:ethernets: fix autopkgtest with alternating default value Gbp-Pq: Name 0004-tests-ethernets-fix-autopkgtest-with-alternating-def.patch --- tests/integration/ethernets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 06ac069..f2fcc4e 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -245,7 +245,8 @@ class _CommonTests(): self.assertIn(b'tcp-segmentation-offload: on', out) self.assertIn(b'tx-tcp6-segmentation: on', out) self.assertIn(b'generic-segmentation-offload: on', out) - self.assertIn(b'generic-receive-offload: off', out) # off by default + # enabled for armhf on autopkgtest.u.c but 'off' elsewhere + # self.assertIn(b'generic-receive-offload: off', out) # validate turning off with open(self.config, 'w') as f: f.write('''network: -- cgit v1.2.3 From e3af3e909975190460e08c7b8787f8501c70d874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 19 May 2022 12:42:11 +0200 Subject: nm: fix rendering of password for unknown/passthrough WPA types (#279) The NetworkManager backend should not take a shortcut and skip rendering the password, in case of an unknown WPA type, as that might be overwritten by a passthrough value. LP: #1972800 Gbp-Pq: Name 0005-nm-fix-rendering-of-password-for-unknown-passthrough.patch --- src/nm.c | 15 ++++++------- tests/parser/test_keyfile.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/nm.c b/src/nm.c index 319a80b..b00a21c 100644 --- a/src/nm.c +++ b/src/nm.c @@ -431,9 +431,6 @@ write_tunnel_params(const NetplanNetDefinition* def, GKeyFile *kf) static void write_dot1x_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) { - if (auth->eap_method == NETPLAN_AUTH_EAP_NONE) - return; - switch (auth->eap_method) { case NETPLAN_AUTH_EAP_TLS: g_key_file_set_string(kf, "802-1x", "eap", "tls"); @@ -468,14 +465,11 @@ write_dot1x_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile static void write_wifi_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) { - if (auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_NONE) - return; - switch (auth->key_management) { + case NETPLAN_AUTH_KEY_MANAGEMENT_NONE: + break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK: g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-psk"); - if (auth->password) - g_key_file_set_string(kf, "wifi-security", "psk", auth->password); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP: g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-eap"); @@ -486,7 +480,10 @@ write_wifi_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile * default: break; // LCOV_EXCL_LINE } - write_dot1x_auth_parameters(auth, kf); + if (auth->eap_method != NETPLAN_AUTH_EAP_NONE) + write_dot1x_auth_parameters(auth, kf); + else if (auth->password) + g_key_file_set_string(kf, "wifi-security", "psk", auth->password); } static void diff --git a/tests/parser/test_keyfile.py b/tests/parser/test_keyfile.py index 809cfd8..7822bbe 100644 --- a/tests/parser/test_keyfile.py +++ b/tests/parser/test_keyfile.py @@ -1242,3 +1242,54 @@ method=auto passthrough: ipv6.ip6-privacy: "-1" '''.format(UUID, UUID)}) + + def test_keyfile_wpa3_sae(self): + self.generate_from_keyfile('''[connection] +id=test2 +uuid={} +type=wifi +interface-name=wlan0 + +[wifi] +mode=infrastructure +ssid=ubuntu-wpa2-wpa3-mixed + +[wifi-security] +key-mgmt=sae +psk=test1234 + +[ipv4] +method=auto + +[ipv6] +addr-gen-mode=stable-privacy +method=auto + +[proxy] +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: + name: "wlan0" + dhcp4: true + dhcp6: true + ipv6-address-generation: "stable-privacy" + access-points: + "ubuntu-wpa2-wpa3-mixed": + auth: + key-management: "none" + password: "test1234" + networkmanager: + uuid: "ff9d6ebc-226d-4f82-a485-b7ff83b9607f" + name: "test2" + passthrough: + wifi-security.key-mgmt: "sae" + ipv6.ip6-privacy: "-1" + proxy._: "" + networkmanager: + uuid: "{}" + name: "test2" +'''.format(UUID, UUID)}) -- cgit v1.2.3 From 65a6fdc604c7e4d8783602a503d8ab3d3c6652d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Wed, 11 May 2022 18:11:20 +0200 Subject: [PATCH] Fix infinite timeouts in ovs-vsctl (Closes: #1000137) (#266) * ovs: time out OVS commands after a while * tests: time out ovs-vsctl commands ovs-vsctl will hang forever if the host is not OVS enabled, blocking the tests Gbp-Pq: Name ovs-timeout.patch --- src/openvswitch.c | 2 +- tests/generator/base.py | 4 +- tests/generator/test_ovs.py | 29 +++++++++++ tests/integration/ovs.py | 117 ++++++++++++++++++++++---------------------- 4 files changed, 91 insertions(+), 61 deletions(-) diff --git a/src/openvswitch.c b/src/openvswitch.c index 7479267..b625588 100644 --- a/src/openvswitch.c +++ b/src/openvswitch.c @@ -59,7 +59,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, g_string_append_printf(s, "After=netplan-ovs-%s.service\n", dependency); } - g_string_append(s, "\n[Service]\nType=oneshot\n"); + g_string_append(s, "\n[Service]\nType=oneshot\nTimeoutStartSec=10s\n"); g_string_append(s, cmds->str); g_string_free_to_file(s, rootdir, path, NULL); diff --git a/tests/generator/base.py b/tests/generator/base.py index d9396dd..da17167 100644 --- a/tests/generator/base.py +++ b/tests/generator/base.py @@ -66,9 +66,9 @@ standalone\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s mcast_snooping_ena Bridge %(iface)s external-ids:netplan/mcast_snooping_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s \ rstp_enable=false\nExecStart=/usr/bin/ovs-vsctl set Bridge %(iface)s external-ids:netplan/rstp_enable=false\n' OVS_BR_EMPTY = _OVS_BASE + 'After=netplan-ovs-cleanup.service\nBefore=network.target\nWants=network.target\n\n[Service]\n\ -Type=oneshot\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT +Type=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/bin/ovs-vsctl --may-exist add-br %(iface)s\n' + OVS_BR_DEFAULT OVS_CLEANUP = _OVS_BASE + 'ConditionFileIsExecutable=/usr/bin/ovs-vsctl\nBefore=network.target\nWants=network.target\n\n\ -[Service]\nType=oneshot\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' +[Service]\nType=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/sbin/netplan apply --only-ovs-cleanup\n' UDEV_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", ATTR{address}=="%s", NAME="%s"\n' UDEV_NO_MAC_RULE = 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="%s", NAME="%s"\n' UDEV_SRIOV_RULE = 'ACTION=="add", SUBSYSTEM=="net", ATTRS{sriov_totalvfs}=="?*", RUN+="/usr/sbin/netplan apply --sriov-only"\n' diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py index dff2989..e09973f 100644 --- a/tests/generator/test_ovs.py +++ b/tests/generator/test_ovs.py @@ -50,6 +50,7 @@ class TestOpenVSwitch(TestBase): self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs0 eth0 @@ -60,6 +61,7 @@ After=netplan-ovs-ovs0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Interface eth0 external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set Interface eth0 other-config:disable-in-band=true @@ -71,6 +73,7 @@ After=netplan-ovs-ovs0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface eth1 other-config:disable-in-band=false ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-config/disable-in-band=false '''}, @@ -109,6 +112,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface eth1 external-ids:netplan/other-confi self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/external-ids/iface-id=myhostname ExecStart=/usr/bin/ovs-vsctl set open_vswitch . other-config:disable-in-band=true @@ -129,6 +133,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/other-confi self.assert_ovs({'ovs0.service': OVS_VIRTUAL % {'iface': 'ovs0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs0 ''' + OVS_BR_DEFAULT % {'iface': 'ovs0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 protocols=OpenFlow10,OpenFlow11,OpenFlow12 @@ -185,6 +190,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -253,6 +259,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=active @@ -318,6 +325,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -357,6 +365,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -408,6 +417,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=activ ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 @@ -432,6 +442,7 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:iface-id=myhostname @@ -462,6 +473,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/other-config/di ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 @@ -521,6 +533,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/rstp_enable=tru ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 protocols=OpenFlow10,OpenFlow11,OpenFlow15 @@ -570,6 +583,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/protocols=OpenF ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'} + '''\ ExecStart=/usr/bin/ovs-vsctl set-controller br0 ptcp: ptcp:1337 ptcp:1337:[fe80::1234%eth0] pssl:1337:[fe80::1] ssl:10.10.10.1 \ @@ -583,6 +597,7 @@ ExecStart=/usr/bin/ovs-vsctl set Controller br0 external-ids:netplan/connection- 'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path '''}, @@ -680,6 +695,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set- self.assert_ovs({'global.service': OVS_VIRTUAL % {'iface': 'global', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set-ssl /key/path /some/path /another/path ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set-ssl=/key/path,/some/path,/another/path '''}, @@ -784,6 +800,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 eth1 eth2 ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -832,6 +849,7 @@ Bond=bond0 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patchx -- set Interface patchx type=patch options:peer=patchy ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, @@ -841,6 +859,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-bond br0 bond0 patchy eth0 -- set Interface patchy type=patch options:peer=patchx ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan=true ExecStart=/usr/bin/ovs-vsctl set Port bond0 lacp=off @@ -852,6 +871,7 @@ After=netplan-ovs-br1.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patchx external-ids:netplan=true '''}, 'patchy.service': OVS_VIRTUAL % {'iface': 'patchy', 'extra': @@ -860,6 +880,7 @@ After=netplan-ovs-bond0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) @@ -887,12 +908,14 @@ ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 patch0-1 -- set Interface patch0-1 type=patch options:peer=patch1-0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'br1.service': OVS_VIRTUAL % {'iface': 'br1', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br1 ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br1 patch1-0 -- set Interface patch1-0 type=patch options:peer=patch0-1 ''' + OVS_BR_DEFAULT % {'iface': 'br1'}}, @@ -902,6 +925,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patch0-1 external-ids:netplan=true '''}, 'patch1-0.service': OVS_VIRTUAL % {'iface': 'patch1-0', 'extra': @@ -910,6 +934,7 @@ After=netplan-ovs-br1.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true '''}, 'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) @@ -934,6 +959,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true self.assert_ovs({'br0.service': OVS_VIRTUAL % {'iface': 'br0', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0 ''' + OVS_BR_DEFAULT % {'iface': 'br0'}}, 'br0.100.service': OVS_VIRTUAL % {'iface': 'br0.100', 'extra': @@ -942,6 +968,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true '''}, @@ -971,6 +998,7 @@ After=netplan-ovs-br0.service [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br br0.100 br0 100 ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true '''}, @@ -1007,6 +1035,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true self.assert_ovs({'ovs-br.service': OVS_VIRTUAL % {'iface': 'ovs-br', 'extra': ''' [Service] Type=oneshot +TimeoutStartSec=10s ExecStart=/usr/bin/ovs-vsctl --may-exist add-br ovs-br ExecStart=/usr/bin/ovs-vsctl --may-exist add-port ovs-br non-ovs-bond ''' + OVS_BR_DEFAULT % {'iface': 'ovs-br'}}, diff --git a/tests/integration/ovs.py b/tests/integration/ovs.py index bfd6eb9..a7f75ef 100644 --- a/tests/integration/ovs.py +++ b/tests/integration/ovs.py @@ -31,8 +31,8 @@ class _CommonTests(): def _collect_ovs_settings(self, bridge0): d = {} - d['show'] = subprocess.check_output(['ovs-vsctl', 'show']) - d['ssl'] = subprocess.check_output(['ovs-vsctl', 'get-ssl']) + d['show'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) + d['ssl'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-ssl']) # Get external-ids for tbl in ('Open_vSwitch', 'Controller', 'Bridge', 'Port', 'Interface'): cols = 'name,external-ids' @@ -40,37 +40,37 @@ class _CommonTests(): cols = 'external-ids' elif tbl == 'Controller': cols = '_uuid,external-ids' - d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', - 'bare', '--no-headings', 'list', tbl]) + d['external-ids-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', tbl]) # Get other-config for tbl in ('Open_vSwitch', 'Bridge', 'Port', 'Interface'): cols = 'name,other-config' if tbl == 'Open_vSwitch': cols = 'other-config' - d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '--columns=%s' % cols, '-f', 'csv', '-d', - 'bare', '--no-headings', 'list', tbl]) + d['other-config-%s' % tbl] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=%s' % cols, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', tbl]) # Get bond settings for col in ('bond_mode', 'lacp'): - d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', - '--no-headings', 'list', 'Port']) + d['%s-Bond' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', 'Port']) # Get bridge settings - d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-fail-mode', bridge0]) + d['set-fail-mode-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-fail-mode', bridge0]) for col in ('mcast_snooping_enable', 'rstp_enable', 'protocols'): - d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '--columns=name,%s' % col, '-f', 'csv', '-d', 'bare', - '--no-headings', 'list', 'Bridge']) + d['%s-Bridge' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,%s' % col, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', 'Bridge']) # Get controller settings - d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', 'get-controller', bridge0]) + d['set-controller-Bridge'] = subprocess.check_output(['ovs-vsctl', '-t', '5', 'get-controller', bridge0]) for col in ('connection_mode',): - d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '--columns=_uuid,%s' % col, '-f', 'csv', '-d', - 'bare', '--no-headings', 'list', 'Controller']) + d['%s-Controller' % col] = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=_uuid,%s' % col, '-f', 'csv', + '-d', 'bare', '--no-headings', 'list', 'Controller']) return d def test_cleanup_interfaces(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) with open(self.config, 'w') as f: f.write('''network: openvswitch: @@ -81,7 +81,7 @@ class _CommonTests(): ovs1: {interfaces: [patch1-0]}''') self.generate_and_settle(['ovs0', 'ovs1']) # Basic verification that the bridges/ports/interfaces are there in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs0', out) self.assertIn(b' Port patch0-1', out) self.assertIn(b' Interface patch0-1', out) @@ -94,7 +94,7 @@ class _CommonTests(): %(ec)s: {addresses: ['1.2.3.4/24']}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client]) # Verify that the netplan=true tagged bridges/ports have been cleaned up - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertNotIn(b'Bridge ovs0', out) self.assertNotIn(b'Port patch0-1', out) self.assertNotIn(b'Interface patch0-1', out) @@ -105,11 +105,11 @@ class _CommonTests(): def test_cleanup_patch_ports(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patchy']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patchy']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -122,7 +122,7 @@ class _CommonTests(): ovs0: {interfaces: [patch0-1, bond0]}''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs0']) # Basic verification that the bridges/ports/interfaces are there in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs0', out) self.assertIn(b' Port patch0-1\n Interface patch0-1\n type: patch', out) self.assertIn(b' Port bond0', out) @@ -141,7 +141,7 @@ class _CommonTests(): self.generate_and_settle([self.dev_e_client, 'ovs1']) # Verify that the netplan=true tagged patch ports have been cleaned up # even though the containing bond0 port still exists (with new patch ports) - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovs1', out) self.assertIn(b' Port patchy\n Interface patchy\n type: patch', out) self.assertIn(b' Port bond0', out) @@ -155,9 +155,9 @@ class _CommonTests(): def test_bridge_vlan(self): self.setup_eth(None, True) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-data']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-data']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) with open(self.config, 'w') as f: f.write('''network: version: 2 @@ -183,7 +183,7 @@ class _CommonTests(): 'br-data', 'br-eth42.100']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertIn(b''' Port %(ec)b Interface %(ec)b''' % {b'ec': self.dev_e_client.encode()}, out) @@ -196,16 +196,16 @@ class _CommonTests(): ['inet 192.168.5.[0-9]+/16', 'mtu 9000']) # from DHCP self.assert_iface('br-data', ['inet 192.168.20.1/16']) self.assert_iface(self.dev_e_client, ['mtu 9000', 'master ovs-system']) - self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', 'br-to-vlan', + self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', '-t', '5', 'br-to-vlan', 'br-%s.100' % self.dev_e_client])) self.assertIn(b'br-%b' % self.dev_e_client.encode(), subprocess.check_output( - ['ovs-vsctl', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) + ['ovs-vsctl', '-t', '5', 'br-to-parent', 'br-%s.100' % self.dev_e_client])) self.assertIn(b'br-%b' % self.dev_e_client.encode(), out) def test_bridge_vlan_deletion(self): self.setup_eth(None, True) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s' % self.dev_e_client]) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br-%s.100' % self.dev_e_client]) with open(self.config, 'w') as f: f.write('''network: version: 2 @@ -228,13 +228,13 @@ class _CommonTests(): 'br-eth42.100']) # Basic verification that the underlying bridge and vlan interface are configured - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertIn(b''' Port br-%(ec)b.100 tag: 100 Interface br-%(ec)b.100 type: internal''' % {b'ec': self.dev_e_client.encode()}, out) - self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', 'br-to-vlan', + self.assertIn(b'100', subprocess.check_output(['ovs-vsctl', '-t', '5', 'br-to-vlan', 'br-%s.100' % self.dev_e_client])) # Write a network configuration that has the .100 vlan interface removed @@ -253,14 +253,14 @@ class _CommonTests(): self.generate_and_settle([self.dev_e_client, self.state_dhcp4('br-eth42')]) # Check that the underlying bridge is still present but the vlan interface is now absent - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br-%b' % self.dev_e_client.encode(), out) self.assertNotIn(b'Port br-%(ec)b.100' % {b'ec': self.dev_e_client.encode()}, out) def test_bridge_base(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) - self.addCleanup(subprocess.call, ['ovs-vsctl', 'del-ssl']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', 'del-ssl']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -282,7 +282,7 @@ class _CommonTests(): ''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) # Basic verification that the interfaces/ports are in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovsbr', out) self.assertIn(b' Controller "tcp:127.0.0.1"', out) self.assertIn(b' Controller "pssl:1337:[::1]"', out) @@ -291,15 +291,15 @@ class _CommonTests(): self.assertIn(b' Port %(ec)b\n Interface %(ec)b' % {b'ec': self.dev_e_client.encode()}, out) self.assertIn(b' Port %(e2c)b\n Interface %(e2c)b' % {b'e2c': self.dev_e2_client.encode()}, out) # Verify the bridge was tagged 'netplan:true' correctly - out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', + out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Bridge', 'ovsbr']) self.assertIn(b'netplan=true', out) self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) def test_bond_base(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovsbr']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'mybond']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovsbr']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'mybond']) with open(self.config, 'w') as f: f.write('''network: ethernets: @@ -318,13 +318,14 @@ class _CommonTests(): interfaces: [mybond]''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovsbr']) # Basic verification that the interfaces/ports are in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge ovsbr', out) self.assertIn(b' Port mybond', out) self.assertIn(b' Interface %b' % self.dev_e_client.encode(), out) self.assertIn(b' Interface %b' % self.dev_e2_client.encode(), out) # Verify the bond was tagged 'netplan:true' correctly - out = subprocess.check_output(['ovs-vsctl', '--columns=name,external-ids', '-f', 'csv', '-d', 'bare', 'list', 'Port']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', '--columns=name,external-ids', '-f', 'csv', + '-d', 'bare', 'list', 'Port']) self.assertIn(b'mybond,netplan=true', out) # Verify bond params out = subprocess.check_output(['ovs-appctl', 'bond/show', 'mybond']) @@ -337,10 +338,10 @@ class _CommonTests(): def test_bridge_patch_ports(self): self.setup_eth(None) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch1-0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'br1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch0-1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'patch1-0']) with open(self.config, 'w') as f: f.write('''network: openvswitch: @@ -355,7 +356,7 @@ class _CommonTests(): interfaces: [patch1-0]''') self.generate_and_settle(['br0', 'br1']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show']) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show']) self.assertIn(b' Bridge br0', out) self.assertIn(b''' Port patch0-1 Interface patch0-1 @@ -371,7 +372,7 @@ class _CommonTests(): def test_bridge_non_ovs_bond(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs-br']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs-br']) self.addCleanup(subprocess.call, ['ip', 'link', 'del', 'non-ovs-bond']) with open(self.config, 'w') as f: f.write('''network: @@ -388,7 +389,7 @@ class _CommonTests(): openvswitch: {}''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle([self.dev_e_client, self.dev_e2_client, 'ovs-br', 'non-ovs-bond']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], universal_newlines=True) self.assertIn(' Bridge ovs-br', out) self.assertIn(''' Port non-ovs-bond Interface non-ovs-bond''', out) @@ -401,7 +402,7 @@ class _CommonTests(): def test_vlan_maas(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', '%s.21' % self.dev_e_client], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -434,7 +435,7 @@ class _CommonTests(): mtu: 1500''' % {'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, 'ovs0', 'eth42.21']) # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) + out = subprocess.check_output(['ovs-vsctl', '-t', '5', 'show'], universal_newlines=True) self.assertIn(' Bridge ovs0', out) self.assertIn(''' Port %(ec)s.21 Interface %(ec)s.21''' % {'ec': self.dev_e_client}, out) @@ -466,9 +467,9 @@ class _CommonTests(): def test_settings_tag_cleanup(self): self.setup_eth(None, False) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs0']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'ovs1']) - self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'bond0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs0']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-br', 'ovs1']) + self.addCleanup(subprocess.call, ['ovs-vsctl', '-t', '5', '--if-exists', 'del-port', 'bond0']) with open(self.config, 'w') as f: f.write('''network: version: 2 -- cgit v1.2.3 From b5b90969ad951e107063d3632b4448ff3f71741d Mon Sep 17 00:00:00 2001 From: Debian netplan Maintainers Date: Sun, 14 Aug 2022 14:58:56 +0200 Subject: autopkgtest-fixes Gbp-Pq: Name autopkgtest-fixes.patch --- tests/integration/base.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index b086385..fffcaa7 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -75,7 +75,7 @@ class IntegrationTestsBase(unittest.TestCase): os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') + f.write('[keyfile]\nunmanaged-devices+=interface-name:en*,eth0,veth42,veth43,nptestsrv') subprocess.check_call(['netplan', 'apply']) subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) @@ -144,12 +144,6 @@ class IntegrationTestsBase(unittest.TestCase): universal_newlines=True) klass.dev_e2_client_mac = out.split()[2] - os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) - - # work around https://launchpad.net/bugs/1615044 - with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: - f.write('[keyfile]\nunmanaged-devices=') - @classmethod def shutdown_devices(klass): '''Remove test devices''' @@ -440,9 +434,10 @@ class IntegrationTestsWifi(IntegrationTestsBase): klass.dev_w_ap = devs[0] klass.dev_w_client = devs[1] + os.makedirs('/run/NetworkManager/conf.d', exist_ok=True) # don't let NM trample over our fake AP with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: - f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=%s\n' % klass.dev_w_ap) @classmethod def shutdown_devices(klass): -- cgit v1.2.3 From 733979d72acff8d3c12ac1cd97c27202fc60d34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 10 Mar 2022 09:41:58 +0100 Subject: cli:apply: fix potential race with rename/creation of netdevs and start networkd if off (LP: #1962095) (#260) Calling networkctl_reload() before networkd_interfaces() makes sure that newly created netdevs will show up in the interface list. Making use of the index number (instead of interface name) ensures that renamed interfaces are properly reconfigured even if they changed their name. Starting networkd if not active is making sure we can process network changes even if systemd-networkd was stopped before (e.g. by subiquity), see LP#1962095 COMMITS: * tests:integration:base: increase debugging output * cli:apply: fix potential race with rename/creation of netdevs Calling networkctl_reload() before networkd_interfaces() makes sure that newly created netdevs will show up in the interface list. Making use of the index number (instead of interface name) ensures that renamed interfaces are properly reconfigured even if they changed their name. * tests:regressions: handle networkd inactive fallback (LP: #1962095) * cli:apply: start networkd if stopped (LP: #1962095) * tests:bonds: do not try to match on MAC while changing it at the same time When a MAC address is set for a bond, networkd will set the same MAC address to the enslaved interfaces. Therefore we cannot match on the original MAC for the ethernet device, as networkd will not find/manage that interface otherwise. https://systemd.network/systemd.netdev.html#MACAddress= Gbp-Pq: Name 0002-cli-apply-fix-potential-race-with-rename-creation-of.patch --- netplan/cli/commands/apply.py | 8 +++++++- netplan/cli/utils.py | 7 +++++-- tests/integration/base.py | 7 ++++++- tests/integration/bonds.py | 4 +--- tests/integration/regressions.py | 16 ++++++++++++++++ tests/test_utils.py | 18 +++++++++++++----- 6 files changed, 48 insertions(+), 12 deletions(-) diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index d481184..b36662a 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -252,7 +252,13 @@ class NetplanApply(utils.NetplanCommand): 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()) + try: + utils.networkctl_reload() + utils.networkctl_reconfigure(utils.networkd_interfaces()) + except subprocess.CalledProcessError: + # (re-)start systemd-networkd if it is not running, yet + logging.warning('Falling back to a hard restart of systemd-networkd.service') + utils.systemctl('restart', ['systemd-networkd.service'], sync=True) # 1st: execute OVS cleanup, to avoid races while applying OVS config utils.systemctl('start', [OVS_CLEANUP_SERVICE], sync=True) # 2nd: start all other services diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py index a05b42b..4fb1dad 100644 --- a/netplan/cli/utils.py +++ b/netplan/cli/utils.py @@ -95,12 +95,15 @@ def networkd_interfaces(): for line in out.splitlines(): s = line.strip().split(' ') if s[0].isnumeric() and s[-1] not in ['unmanaged', 'linger']: - interfaces.add(s[1]) + interfaces.add(s[0]) return interfaces -def networkctl_reconfigure(interfaces): +def networkctl_reload(): subprocess.check_call(['networkctl', 'reload']) + + +def networkctl_reconfigure(interfaces): if len(interfaces) >= 1: subprocess.check_call(['networkctl', 'reconfigure'] + list(interfaces)) diff --git a/tests/integration/base.py b/tests/integration/base.py index fffcaa7..10094dd 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -294,7 +294,12 @@ class IntegrationTestsBase(unittest.TestCase): cmd = ['netplan', 'apply'] if state_dir: cmd = cmd + ['--state', state_dir] - out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, universal_newlines=True) + out = '' + try: + out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, universal_newlines=True) + except subprocess.CalledProcessError as e: + self.assertTrue(False, 'netplan apply failed: {}'.format(e.output)) + if 'Run \'systemctl daemon-reload\' to reload units.' in out: self.fail('systemd units changed without reload') # start NM so that we can verify that it does not manage anything diff --git a/tests/integration/bonds.py b/tests/integration/bonds.py index 763f7e5..2e3773e 100644 --- a/tests/integration/bonds.py +++ b/tests/integration/bonds.py @@ -315,14 +315,12 @@ class TestNetworkd(IntegrationTestsBase, _CommonTests): ethbn: match: name: %(ec)s - macaddress: %(ec_mac)s bonds: mybond: interfaces: [ethbn] macaddress: 00:01:02:03:04:05 dhcp4: yes''' % {'r': self.backend, - 'ec': self.dev_e_client, - 'ec_mac': self.dev_e_client_mac}) + 'ec': self.dev_e_client}) self.generate_and_settle([self.dev_e_client, self.state_dhcp4('mybond')]) self.assert_iface_up(self.dev_e_client, ['master mybond'], ['inet ']) self.assert_iface_up('mybond', ['inet 192.168.5.[0-9]+/24', '00:01:02:03:04:05']) diff --git a/tests/integration/regressions.py b/tests/integration/regressions.py index 40aeeac..7d8cabb 100644 --- a/tests/integration/regressions.py +++ b/tests/integration/regressions.py @@ -118,6 +118,22 @@ r'Press ENTER before the timeout to accept the new configuration\n\n\n' r'(Changes will revert in \d+ seconds\n)+' r'Reverting\.') + def test_apply_networkd_inactive_lp1962095(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + ethernets: + %(ec)s: + dhcp4: true + %(e2c)s: + dhcp4: true + version: 2''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + # stop networkd to simulate the failure case + subprocess.check_call(['systemctl', 'stop', 'systemd-networkd.service', 'systemd-networkd.socket']) + self.generate_and_settle([self.state_dhcp4(self.dev_e_client), self.state_dhcp4(self.dev_e2_client)]) + self.assert_iface_up(self.dev_e_client, ['inet 192.168.5.[0-9]+/24']) + self.assert_iface_up(self.dev_e2_client, ['inet 192.168.6.[0-9]+/24']) + @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") diff --git a/tests/test_utils.py b/tests/test_utils.py index b958d58..bd035a9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -215,17 +215,25 @@ class TestUtils(unittest.TestCase): 174 wwan0 wwan off linger''') res = utils.networkd_interfaces() self.assertEquals(self.mock_networkctl.calls(), [['networkctl', '--no-pager', '--no-legend']]) - self.assertIn('wlan0', res) - self.assertIn('ens3', res) + self.assertIn('2', res) + self.assertIn('3', res) + + def test_networkctl_reload(self): + self.mock_networkctl = MockCmd('networkctl') + path_env = os.environ['PATH'] + os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env + utils.networkctl_reload() + self.assertEquals(self.mock_networkctl.calls(), [ + ['networkctl', 'reload'] + ]) def test_networkctl_reconfigure(self): self.mock_networkctl = MockCmd('networkctl') path_env = os.environ['PATH'] os.environ['PATH'] = os.path.dirname(self.mock_networkctl.path) + os.pathsep + path_env - utils.networkctl_reconfigure(['eth0', 'eth1']) + utils.networkctl_reconfigure(['3', '5']) self.assertEquals(self.mock_networkctl.calls(), [ - ['networkctl', 'reload'], - ['networkctl', 'reconfigure', 'eth0', 'eth1'] + ['networkctl', 'reconfigure', '3', '5'] ]) def test_is_nm_snap_enabled(self): -- cgit v1.2.3 From 65f483f9bc9413cea0a84ea4af2ef321762971e3 Mon Sep 17 00:00:00 2001 From: Nicolas Bock Date: Thu, 12 May 2022 01:18:36 -0600 Subject: Add tristate type for offload options (LP: #1956264) (#270) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: https://bugs.launchpad.net/netplan/+bug/1956264 COMMITS: * Add tristate type for offload options * Clarify name matching issues for `networkd` * Fix tests for offload * cli: apply: properly change udev/.link offloading settings * Re-used existing offloading fields, they are ABI compatible Size: Both enum and gboolean reduce to integer, so they are of same size. Content: An old consumer looking at these will interpret UNSET as if it was TRUE, which is the kernel's default (=UNSET) value. * CI: quirk to add ethtool test dependency * tests: ethernets: link offloading validation * doc: be more specific with the offloading docs Co-authored-by: Lukas Märdian Gbp-Pq: Name 0003-Add-tristate-type-for-offload-options-LP-1956264-270.patch --- doc/netplan.md | 36 ++++++++++++------------ netplan/cli/commands/apply.py | 11 +++++++- src/netplan.c | 21 +++++++++----- src/networkd.c | 49 ++++++++++++++++++-------------- src/parse.c | 55 +++++++++++++++++++++++++++++++----- src/types.c | 8 ++++++ src/types.h | 35 ++++++++++++++++++----- tests/generator/test_ethernets.py | 34 +++++++++++++++------- tests/integration/ethernets.py | 59 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 237 insertions(+), 71 deletions(-) diff --git a/doc/netplan.md b/doc/netplan.md index e9b4e92..373e05d 100644 --- a/doc/netplan.md +++ b/doc/netplan.md @@ -79,6 +79,10 @@ Virtual devices ## Common properties for physical device types +**Note:** Some options will not work reliably for devices matched by name only +and rendered by networkd, due to interactions with device renaming in udev. +Match devices by MAC when setting options like: ``wakeonlan`` or ``*-offload``. + ``match`` (mapping) : This selects a subset of available physical devices by various hardware @@ -139,54 +143,50 @@ Virtual devices : Enable wake on LAN. Off by default. - **Note:** This will not work reliably for devices matched by name - only and rendered by networkd, due to interactions with device - renaming in udev. Match devices by MAC when setting wake on LAN. - ``emit-lldp`` (bool) – since **0.99** : (networkd backend only) Whether to emit LLDP packets. Off by default. ``receive-checksum-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the hardware offload for - checksumming of ingress network packets is enabled. When unset, +: (networkd backend only) If set to true (false), the hardware offload for + checksumming of ingress network packets is enabled (disabled). When unset, the kernel's default will be used. ``transmit-checksum-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the hardware offload for - checksumming of egress network packets is enabled. When unset, +: (networkd backend only) If set to true (false), the hardware offload for + checksumming of egress network packets is enabled (disabled). When unset, the kernel's default will be used. ``tcp-segmentation-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the TCP Segmentation - Offload (TSO) is enabled. When unset, the kernel's default will +: (networkd backend only) If set to true (false), the TCP Segmentation + Offload (TSO) is enabled (disabled). When unset, the kernel's default will be used. ``tcp6-segmentation-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the TCP6 Segmentation - Offload (tx-tcp6-segmentation) is enabled. When unset, the +: (networkd backend only) If set to true (false), the TCP6 Segmentation + Offload (tx-tcp6-segmentation) is enabled (disabled). When unset, the kernel's default will be used. ``generic-segmentation-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the Generic Segmentation - Offload (GSO) is enabled. When unset, the kernel's default will +: (networkd backend only) If set to true (false), the Generic Segmentation + Offload (GSO) is enabled (disabled). When unset, the kernel's default will be used. ``generic-receive-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the Generic Receive - Offload (GRO) is enabled. When unset, the kernel's default will +: (networkd backend only) If set to true (false), the Generic Receive + Offload (GRO) is enabled (disabled). When unset, the kernel's default will be used. ``large-receive-offload`` (bool) – since **0.104** -: (networkd backend only) If set to true, the Generic Receive - Offload (GRO) is enabled. When unset, the kernel's default will +: (networkd backend only) If set to true (false), the Large Receive Offload + (LRO) is enabled (disabled). When unset, the kernel's default will be used. ``openvswitch`` (mapping) – since **0.100** diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py index b36662a..9d4511f 100644 --- a/netplan/cli/commands/apply.py +++ b/netplan/cli/commands/apply.py @@ -221,13 +221,22 @@ class NetplanApply(utils.NetplanCommand): '/sys/class/net/' + device], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.check_call(['udevadm', 'test', + '/sys/class/net/' + device], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) except subprocess.CalledProcessError: logging.debug('Ignoring device without syspath: %s', device) + devices_after_udev = netifaces.interfaces() # apply some more changes manually for iface, settings in changes.items(): # rename non-critical network interfaces - if settings.get('name'): + new_name = settings.get('name') + if new_name: + if iface in devices and new_name in devices_after_udev: + logging.debug('Interface rename {} -> {} already happened.'.format(iface, new_name)) + continue # re-name already happened via 'udevadm test' # bring down the interface, using its current (matched) interface name subprocess.check_call(['ip', 'link', 'set', 'dev', iface, 'down'], stdout=subprocess.DEVNULL, diff --git a/src/netplan.c b/src/netplan.c index 7b387b4..d1a27a6 100644 --- a/src/netplan.c +++ b/src/netplan.c @@ -752,13 +752,20 @@ _serialize_yaml( YAML_BOOL_TRUE(def, event, emitter, "wakeonlan", def->wake_on_lan); /* Offload options */ - YAML_BOOL_TRUE(def, event, emitter, "receive-checksum-offload", def->receive_checksum_offload); - YAML_BOOL_TRUE(def, event, emitter, "transmit-checksum-offload", def->transmit_checksum_offload); - YAML_BOOL_TRUE(def, event, emitter, "tcp-segmentation-offload", def->tcp_segmentation_offload); - YAML_BOOL_TRUE(def, event, emitter, "tcp6-segmentation-offload", def->tcp6_segmentation_offload); - YAML_BOOL_TRUE(def, event, emitter, "generic-segmentation-offload", def->generic_segmentation_offload); - YAML_BOOL_TRUE(def, event, emitter, "generic-receive-offload", def->generic_receive_offload); - YAML_BOOL_TRUE(def, event, emitter, "large-receive-offload", def->large_receive_offload); + if (def->receive_checksum_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "receive-checksum-offload", def->receive_checksum_offload); + if (def->transmit_checksum_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "transmit-checksum-offload", def->transmit_checksum_offload); + if (def->tcp_segmentation_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "tcp-segmentation-offload", def->tcp_segmentation_offload); + if (def->tcp6_segmentation_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "tcp6-segmentation-offload", def->tcp6_segmentation_offload); + if (def->generic_segmentation_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "generic-segmentation-offload", def->generic_segmentation_offload); + if (def->generic_receive_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "generic-receive-offload", def->generic_receive_offload); + if (def->large_receive_offload != NETPLAN_TRISTATE_UNSET) + YAML_BOOL_TRUE(def, event, emitter, "large-receive-offload", def->large_receive_offload); if (def->wowlan && def->wowlan != NETPLAN_WIFI_WOWLAN_DEFAULT) { YAML_SCALAR_PLAIN(event, emitter, "wakeonwlan"); diff --git a/src/networkd.c b/src/networkd.c index 6d26047..62c87ce 100644 --- a/src/networkd.c +++ b/src/networkd.c @@ -243,13 +243,13 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char if (!def->set_name && !def->wake_on_lan && !def->mtubytes && - !def->receive_checksum_offload && - !def->transmit_checksum_offload && - !def->tcp_segmentation_offload && - !def->tcp6_segmentation_offload && - !def->generic_segmentation_offload && - !def->generic_receive_offload && - !def->large_receive_offload) + (def->receive_checksum_offload == NETPLAN_TRISTATE_UNSET) && + (def->transmit_checksum_offload == NETPLAN_TRISTATE_UNSET) && + (def->tcp_segmentation_offload == NETPLAN_TRISTATE_UNSET) && + (def->tcp6_segmentation_offload == NETPLAN_TRISTATE_UNSET) && + (def->generic_segmentation_offload == NETPLAN_TRISTATE_UNSET) && + (def->generic_receive_offload == NETPLAN_TRISTATE_UNSET) && + (def->large_receive_offload == NETPLAN_TRISTATE_UNSET)) return; /* build file contents */ @@ -265,26 +265,33 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char g_string_append_printf(s, "MTUBytes=%u\n", def->mtubytes); /* Offload options */ - if (def->receive_checksum_offload) - g_string_append_printf(s, "ReceiveChecksumOffload=%u\n", def->receive_checksum_offload); + if (def->receive_checksum_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "ReceiveChecksumOffload=%s\n", + (def->receive_checksum_offload ? "true" : "false")); - if (def->transmit_checksum_offload) - g_string_append_printf(s, "TransmitChecksumOffload=%u\n", def->transmit_checksum_offload); + if (def->transmit_checksum_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "TransmitChecksumOffload=%s\n", + (def->transmit_checksum_offload ? "true" : "false")); - if (def->tcp_segmentation_offload) - g_string_append_printf(s, "TCPSegmentationOffload=%u\n", def->tcp_segmentation_offload); + if (def->tcp_segmentation_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "TCPSegmentationOffload=%s\n", + (def->tcp_segmentation_offload ? "true" : "false")); - if (def->tcp6_segmentation_offload) - g_string_append_printf(s, "TCP6SegmentationOffload=%u\n", def->tcp6_segmentation_offload); + if (def->tcp6_segmentation_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "TCP6SegmentationOffload=%s\n", + (def->tcp6_segmentation_offload ? "true" : "false")); - if (def->generic_segmentation_offload) - g_string_append_printf(s, "GenericSegmentationOffload=%u\n", def->generic_segmentation_offload); + if (def->generic_segmentation_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "GenericSegmentationOffload=%s\n", + (def->generic_segmentation_offload ? "true" : "false")); - if (def->generic_receive_offload) - g_string_append_printf(s, "GenericReceiveOffload=%u\n", def->generic_receive_offload); + if (def->generic_receive_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "GenericReceiveOffload=%s\n", + (def->generic_receive_offload ? "true" : "false")); - if (def->large_receive_offload) - g_string_append_printf(s, "LargeReceiveOffload=%u\n", def->large_receive_offload); + if (def->large_receive_offload != NETPLAN_TRISTATE_UNSET) + g_string_append_printf(s, "LargeReceiveOffload=%s\n", + (def->large_receive_offload ? "true" : "false")); orig_umask = umask(022); g_string_free_to_file(s, rootdir, path, ".link"); diff --git a/src/parse.c b/src/parse.c index 350c508..0cb07d2 100644 --- a/src/parse.c +++ b/src/parse.c @@ -370,6 +370,37 @@ handle_generic_bool(NetplanParser* npp, yaml_node_t* node, void* entryptr, const return TRUE; } +/* + * Handler for setting a HashTable field from a mapping node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the boolean field to write is located + */ +static gboolean +handle_generic_tristate(NetplanParser* npp, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + g_assert(entryptr); + NetplanTristate v; + guint offset = GPOINTER_TO_UINT(data); + NetplanTristate* dest = ((void*) entryptr + offset); + + if (g_ascii_strcasecmp(scalar(node), "true") == 0 || + g_ascii_strcasecmp(scalar(node), "on") == 0 || + g_ascii_strcasecmp(scalar(node), "yes") == 0 || + g_ascii_strcasecmp(scalar(node), "y") == 0) + v = NETPLAN_TRISTATE_TRUE; + else if (g_ascii_strcasecmp(scalar(node), "false") == 0 || + g_ascii_strcasecmp(scalar(node), "off") == 0 || + g_ascii_strcasecmp(scalar(node), "no") == 0 || + g_ascii_strcasecmp(scalar(node), "n") == 0) + v = NETPLAN_TRISTATE_FALSE; + else + return yaml_error(npp, node, error, "invalid boolean value '%s'", scalar(node)); + + *dest = v; + mark_data_as_dirty(npp, dest); + return TRUE; +} + /* * Handler for setting a HashTable field from a mapping node, inside a given struct * @entryptr: pointer to the beginning of the to-be-modified data structure @@ -516,6 +547,16 @@ handle_netdef_bool(NetplanParser* npp, yaml_node_t* node, const void* data, GErr return handle_generic_bool(npp, node, npp->current.netdef, data, error); } +/** + * Generic handler for tri-state settings that can bei "UNSET", "TRUE", or "FALSE". + * @data: offset into NetplanNetDefinition where the guint field to write is located + */ +static gboolean +handle_netdef_tristate(NetplanParser* npp, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_tristate(npp, node, npp->current.netdef, data, error); +} + /** * Generic handler for setting a npp->current.netdef guint field from a scalar node * @data: offset into NetplanNetDefinition where the guint field to write is located @@ -2356,13 +2397,13 @@ static const mapping_entry_handler dhcp6_overrides_handlers[] = { {"wakeonlan", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(wake_on_lan)}, \ {"wakeonwlan", YAML_SEQUENCE_NODE, {.generic=handle_wowlan}, netdef_offset(wowlan)}, \ {"emit-lldp", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(emit_lldp)}, \ - {"receive-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(receive_checksum_offload)}, \ - {"transmit-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(transmit_checksum_offload)}, \ - {"tcp-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(tcp_segmentation_offload)}, \ - {"tcp6-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(tcp6_segmentation_offload)}, \ - {"generic-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(generic_segmentation_offload)}, \ - {"generic-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(generic_receive_offload)}, \ - {"large-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_bool}, netdef_offset(large_receive_offload)} + {"receive-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(receive_checksum_offload)}, \ + {"transmit-checksum-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(transmit_checksum_offload)}, \ + {"tcp-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(tcp_segmentation_offload)}, \ + {"tcp6-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(tcp6_segmentation_offload)}, \ + {"generic-segmentation-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(generic_segmentation_offload)}, \ + {"generic-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(generic_receive_offload)}, \ + {"large-receive-offload", YAML_SCALAR_NODE, {.generic=handle_netdef_tristate}, netdef_offset(large_receive_offload)} static const mapping_entry_handler ethernet_def_handlers[] = { COMMON_LINK_HANDLERS, diff --git a/src/types.c b/src/types.c index eb9f780..00c2b0f 100644 --- a/src/types.c +++ b/src/types.c @@ -335,6 +335,14 @@ reset_netdef(NetplanNetDefinition* netdef, NetplanDefType new_type, NetplanBacke reset_private_netdef_data(netdef->_private); FREE_AND_NULLIFY(netdef->_private); + + netdef->receive_checksum_offload = NETPLAN_TRISTATE_UNSET; + netdef->transmit_checksum_offload = NETPLAN_TRISTATE_UNSET; + netdef->tcp_segmentation_offload = NETPLAN_TRISTATE_UNSET; + netdef->tcp6_segmentation_offload = NETPLAN_TRISTATE_UNSET; + netdef->generic_segmentation_offload = NETPLAN_TRISTATE_UNSET; + netdef->generic_receive_offload = NETPLAN_TRISTATE_UNSET; + netdef->large_receive_offload = NETPLAN_TRISTATE_UNSET; } static void diff --git a/src/types.h b/src/types.h index 27a23fc..710b1f1 100644 --- a/src/types.h +++ b/src/types.h @@ -171,6 +171,27 @@ typedef union { } networkd; } NetplanBackendSettings; +typedef enum +{ + /** + * @brief Tristate enum type + * + * This type defines a boolean which can be unset, i.e. + * this type has three states. The enum is ordered so + * that + * + * UNSET -> -1 + * FALSE -> 0 + * TRUE -> 1 + * + * And the integer values can be used directly when + * converting to string. + */ + NETPLAN_TRISTATE_UNSET = -1, /* -1 */ + NETPLAN_TRISTATE_FALSE, /* 0 */ + NETPLAN_TRISTATE_TRUE, /* 1 */ +} NetplanTristate; + struct netplan_net_definition { NetplanDefType type; NetplanBackend backend; @@ -333,13 +354,13 @@ struct netplan_net_definition { gboolean ignore_carrier; /* offload options */ - gboolean receive_checksum_offload; - gboolean transmit_checksum_offload; - gboolean tcp_segmentation_offload; - gboolean tcp6_segmentation_offload; - gboolean generic_segmentation_offload; - gboolean generic_receive_offload; - gboolean large_receive_offload; + NetplanTristate receive_checksum_offload; + NetplanTristate transmit_checksum_offload; + NetplanTristate tcp_segmentation_offload; + NetplanTristate tcp6_segmentation_offload; + NetplanTristate generic_segmentation_offload; + NetplanTristate generic_receive_offload; + NetplanTristate large_receive_offload; struct private_netdef_data* _private; diff --git a/tests/generator/test_ethernets.py b/tests/generator/test_ethernets.py index 46bf764..e81941b 100644 --- a/tests/generator/test_ethernets.py +++ b/tests/generator/test_ethernets.py @@ -772,11 +772,11 @@ method=ignore ethernets: eth1: receive-checksum-offload: true - transmit-checksum-offload: true + transmit-checksum-offload: off tcp-segmentation-offload: true - tcp6-segmentation-offload: true + tcp6-segmentation-offload: false generic-segmentation-offload: true - generic-receive-offload: true + generic-receive-offload: no large-receive-offload: true''') self.assert_networkd({'eth1.link': '''[Match] @@ -784,13 +784,13 @@ OriginalName=eth1 [Link] WakeOnLan=off -ReceiveChecksumOffload=1 -TransmitChecksumOffload=1 -TCPSegmentationOffload=1 -TCP6SegmentationOffload=1 -GenericSegmentationOffload=1 -GenericReceiveOffload=1 -LargeReceiveOffload=1 +ReceiveChecksumOffload=true +TransmitChecksumOffload=false +TCPSegmentationOffload=true +TCP6SegmentationOffload=false +GenericSegmentationOffload=true +GenericReceiveOffload=false +LargeReceiveOffload=true ''', 'eth1.network': '''[Match] Name=eth1 @@ -799,3 +799,17 @@ Name=eth1 LinkLocalAddressing=ipv6 '''}) self.assert_networkd_udev(None) + + def test_offload_invalid(self): + err = self.generate('''network: + version: 2 + ethernets: + eth1: + generic-receive-offload: n + receive-checksum-offload: true + tcp-segmentation-offload: true + tcp6-segmentation-offload: false + generic-segmentation-offload: true + transmit-checksum-offload: xx + large-receive-offload: true''', expect_fail=True) + self.assertIn('invalid boolean value \'xx\'', err) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 865c0d4..06ac069 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -236,6 +236,65 @@ class _CommonTests(): self.assert_iface_up('iface1', ['inet 10.10.10.11']) self.assert_iface_up('iface2', ['inet 10.10.10.22']) + def test_link_offloading(self): + self.setup_eth(None, False) + # check kernel defaults + out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) + self.assertIn(b'rx-checksumming: on', out) + self.assertIn(b'tx-checksumming: on', out) + self.assertIn(b'tcp-segmentation-offload: on', out) + self.assertIn(b'tx-tcp6-segmentation: on', out) + self.assertIn(b'generic-segmentation-offload: on', out) + self.assertIn(b'generic-receive-offload: off', out) # off by default + # validate turning off + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: [10.10.10.22/24] + receive-checksum-offload: off + transmit-checksum-offload: off + tcp-segmentation-offload: off + tcp6-segmentation-offload: off + generic-segmentation-offload: off + generic-receive-offload: off + #large-receive-offload: off # not possible on veth +''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 10.10.10.22']) + out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) + self.assertIn(b'rx-checksumming: off', out) + self.assertIn(b'tx-checksumming: off', out) + self.assertIn(b'tcp-segmentation-offload: off', out) + self.assertIn(b'tx-tcp6-segmentation: off', out) + self.assertIn(b'generic-segmentation-offload: off', out) + self.assertIn(b'generic-receive-offload: off', out) + # validate turning on + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: [10.10.10.22/24] + receive-checksum-offload: true + transmit-checksum-offload: true + tcp-segmentation-offload: true + tcp6-segmentation-offload: true + generic-segmentation-offload: true + generic-receive-offload: true + #large-receive-offload: true # not possible on veth +''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle([self.dev_e_client]) + self.assert_iface_up(self.dev_e_client, ['inet 10.10.10.22']) + out = subprocess.check_output(['ethtool', '-k', self.dev_e_client]) + self.assertIn(b'rx-checksumming: on', out) + self.assertIn(b'tx-checksumming: on', out) + self.assertIn(b'tcp-segmentation-offload: on', out) + self.assertIn(b'tx-tcp6-segmentation: on', out) + self.assertIn(b'generic-segmentation-offload: on', out) + self.assertIn(b'generic-receive-offload: on', out) + @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") -- cgit v1.2.3 From 77b8fcc610f1f1ef35eb2d69cfaba04dadacf4e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 19 May 2022 12:43:11 +0200 Subject: tests:ethernets: fix autopkgtest with alternating default value Gbp-Pq: Name 0004-tests-ethernets-fix-autopkgtest-with-alternating-def.patch --- tests/integration/ethernets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/ethernets.py b/tests/integration/ethernets.py index 06ac069..f2fcc4e 100644 --- a/tests/integration/ethernets.py +++ b/tests/integration/ethernets.py @@ -245,7 +245,8 @@ class _CommonTests(): self.assertIn(b'tcp-segmentation-offload: on', out) self.assertIn(b'tx-tcp6-segmentation: on', out) self.assertIn(b'generic-segmentation-offload: on', out) - self.assertIn(b'generic-receive-offload: off', out) # off by default + # enabled for armhf on autopkgtest.u.c but 'off' elsewhere + # self.assertIn(b'generic-receive-offload: off', out) # validate turning off with open(self.config, 'w') as f: f.write('''network: -- cgit v1.2.3 From a0def95eddc5a4ca4555a288f0082c0d3e0d4880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= Date: Thu, 19 May 2022 12:42:11 +0200 Subject: nm: fix rendering of password for unknown/passthrough WPA types (#279) The NetworkManager backend should not take a shortcut and skip rendering the password, in case of an unknown WPA type, as that might be overwritten by a passthrough value. LP: #1972800 Gbp-Pq: Name 0005-nm-fix-rendering-of-password-for-unknown-passthrough.patch --- src/nm.c | 15 ++++++------- tests/parser/test_keyfile.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/nm.c b/src/nm.c index 319a80b..b00a21c 100644 --- a/src/nm.c +++ b/src/nm.c @@ -431,9 +431,6 @@ write_tunnel_params(const NetplanNetDefinition* def, GKeyFile *kf) static void write_dot1x_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) { - if (auth->eap_method == NETPLAN_AUTH_EAP_NONE) - return; - switch (auth->eap_method) { case NETPLAN_AUTH_EAP_TLS: g_key_file_set_string(kf, "802-1x", "eap", "tls"); @@ -468,14 +465,11 @@ write_dot1x_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile static void write_wifi_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) { - if (auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_NONE) - return; - switch (auth->key_management) { + case NETPLAN_AUTH_KEY_MANAGEMENT_NONE: + break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK: g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-psk"); - if (auth->password) - g_key_file_set_string(kf, "wifi-security", "psk", auth->password); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP: g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-eap"); @@ -486,7 +480,10 @@ write_wifi_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile * default: break; // LCOV_EXCL_LINE } - write_dot1x_auth_parameters(auth, kf); + if (auth->eap_method != NETPLAN_AUTH_EAP_NONE) + write_dot1x_auth_parameters(auth, kf); + else if (auth->password) + g_key_file_set_string(kf, "wifi-security", "psk", auth->password); } static void diff --git a/tests/parser/test_keyfile.py b/tests/parser/test_keyfile.py index 809cfd8..7822bbe 100644 --- a/tests/parser/test_keyfile.py +++ b/tests/parser/test_keyfile.py @@ -1242,3 +1242,54 @@ method=auto passthrough: ipv6.ip6-privacy: "-1" '''.format(UUID, UUID)}) + + def test_keyfile_wpa3_sae(self): + self.generate_from_keyfile('''[connection] +id=test2 +uuid={} +type=wifi +interface-name=wlan0 + +[wifi] +mode=infrastructure +ssid=ubuntu-wpa2-wpa3-mixed + +[wifi-security] +key-mgmt=sae +psk=test1234 + +[ipv4] +method=auto + +[ipv6] +addr-gen-mode=stable-privacy +method=auto + +[proxy] +'''.format(UUID)) + self.assert_netplan({UUID: '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: + name: "wlan0" + dhcp4: true + dhcp6: true + ipv6-address-generation: "stable-privacy" + access-points: + "ubuntu-wpa2-wpa3-mixed": + auth: + key-management: "none" + password: "test1234" + networkmanager: + uuid: "ff9d6ebc-226d-4f82-a485-b7ff83b9607f" + name: "test2" + passthrough: + wifi-security.key-mgmt: "sae" + ipv6.ip6-privacy: "-1" + proxy._: "" + networkmanager: + uuid: "{}" + name: "test2" +'''.format(UUID, UUID)}) -- cgit v1.2.3