diff -Nru netplan.io-0.100/dbus/io.netplan.Netplan.conf netplan.io-0.101/dbus/io.netplan.Netplan.conf --- netplan.io-0.100/dbus/io.netplan.Netplan.conf 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/dbus/io.netplan.Netplan.conf 2020-12-09 11:32:25.000000000 +0000 @@ -11,6 +11,8 @@ + diff -Nru netplan.io-0.100/debian/changelog netplan.io-0.101/debian/changelog --- netplan.io-0.100/debian/changelog 2020-10-19 12:49:52.000000000 +0000 +++ netplan.io-0.101/debian/changelog 2021-01-08 14:17:07.000000000 +0000 @@ -1,3 +1,50 @@ +netplan.io (0.101-0ubuntu3~20.04.2) focal; urgency=medium + + * Backport netplan.io 0.101-0ubuntu3 to 20.04 (LP: #1908509) + - Includes DBus Config/Get/Set/Try API + - Includes fixes for NetworkManager integration + - Includes Documentation improvements + - Compatibility with systemd v247 + * Improve test stability, by adding two patches from upstream: + - debian/patches/0004-tests-tunnels-improve-test-reliability.patch + - debian/patches/0005-tests-dbus-improve-test-stability-of-timeouts.patch + + -- Lukas Märdian Fri, 08 Jan 2021 15:17:07 +0100 + +netplan.io (0.101-0ubuntu3) hirsute; urgency=medium + + * Add d/p/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch: + Allows parsing of networkmanager: backend handlers for modem devices + + -- Lukas Märdian Thu, 17 Dec 2020 10:49:43 +0100 + +netplan.io (0.101-0ubuntu2) hirsute; urgency=medium + + * 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. + + -- Lukas Märdian Wed, 16 Dec 2020 13:43:51 +0100 + +netplan.io (0.101-0ubuntu1) hirsute; urgency=medium + + * New upstream release: 0.101 + - Documentation improvements + - Improved integration tests + - Add more examples for Wireguard, Open vSwitch, DBus + - Improve test stability + - Implementation of DBus Config/Get/Set/Try APIs + - Add per-route MTU option (LP: #1860201) + Bug fixes: + - Fix MAAS OVS first boot (LP: #1898997) + - Fix match of duplicate MAC on VLANs (LP: #1888726) + - Fix crash in Python parser (LP: #1904633) (LP: #1905156) + - Fix rename of matched interfaces at runtime (LP: #1904662) + * Drop all distro patches, which have been integrated upstream + * Update symbols file + + -- Lukas Märdian Wed, 09 Dec 2020 09:41:50 +0100 + netplan.io (0.100-0ubuntu4~20.04.3) focal; urgency=medium * debian/control:netplan.io: Suggest openvswitch-switch runtime dependency @@ -10,6 +57,17 @@ -- Lukas Märdian Mon, 19 Oct 2020 14:49:52 +0200 +netplan.io (0.100-0ubuntu5) groovy; urgency=medium + + * debian/control:netplan.io: Suggest openvswitch-switch runtime dependency + * Add d/p/0002-tests-tunnels-improve-WG-handshake-regex.patch + and d/p/0003-tests-ovs-fix-OVS-timeouts.patch + - Improve stability of autopkgtests + * Add d/p/0004-Fix-MAAS-OVS-first-boot-for-single-NIC-PXE-systems-1.patch + - Setup OVS early in network-pre.target to avoid delays (LP: #1898997) + + -- Lukas Märdian Wed, 14 Oct 2020 11:29:03 +0200 + netplan.io (0.100-0ubuntu4~20.04.2) focal; urgency=medium * Backport netplan.io 0.100-0ubuntu4 to 20.04 (LP: #1894197) diff -Nru netplan.io-0.100/debian/libnetplan0.symbols netplan.io-0.101/debian/libnetplan0.symbols --- netplan.io-0.100/debian/libnetplan0.symbols 2020-10-19 12:49:52.000000000 +0000 +++ netplan.io-0.101/debian/libnetplan0.symbols 2020-12-17 11:33:20.000000000 +0000 @@ -3,6 +3,7 @@ NETPLAN_WIFI_WOWLAN_TYPES@Base 0.99 address_option_handlers@Base 0.100 current_file@Base 0.99 + find_yaml_glob@Base 0.101 g_string_free_to_file@Base 0.99 is_hostname@Base 0.100 is_ip4_address@Base 0.99 @@ -12,6 +13,7 @@ missing_ids_found@Base 0.99 netdefs@Base 0.99 netdefs_ordered@Base 0.99 + netplan_clear_netdefs@Base 0.101 netplan_finish_parse@Base 0.99 netplan_get_global_backend@Base 0.99 netplan_parse_yaml@Base 0.99 diff -Nru netplan.io-0.100/debian/patches/0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch netplan.io-0.101/debian/patches/0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch --- netplan.io-0.100/debian/patches/0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/debian/patches/0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch 2021-01-08 14:05:14.000000000 +0000 @@ -0,0 +1,129 @@ +From: =?utf-8?q?Lukas_M=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 +--- + 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]) + diff -Nru netplan.io-0.100/debian/patches/0001-Implement-just-in-time-behaviour-for-generate-162.patch netplan.io-0.101/debian/patches/0001-Implement-just-in-time-behaviour-for-generate-162.patch --- netplan.io-0.100/debian/patches/0001-Implement-just-in-time-behaviour-for-generate-162.patch 2020-10-19 12:49:52.000000000 +0000 +++ netplan.io-0.101/debian/patches/0001-Implement-just-in-time-behaviour-for-generate-162.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,296 +0,0 @@ -From: Dimitri John Ledkov <19779+xnox@users.noreply.github.com> -Date: Wed, 9 Sep 2020 12:21:06 +0100 -Subject: Implement just-in-time behaviour for generate (#162) -MIME-Version: 1.0 -Content-Type: text/plain; charset="utf-8" -Content-Transfer-Encoding: 8bit - -If system is initializing (basic.target not reached yet), and netplan generate is called ensure that any netplan generated service units are added to the initial boot transaction. - -This should resolve cloud-init first-time booting with systemd-networkd disabled, or with requirement to add wlan/wifi units. - -Commits: -* generate: implement just-in-time behaviour of generate - -If system is initializing (basic.target not reached yet), and `netplan -generate` is called ensure that any netplan generated service units -are added to the initial boot transaction. - -This should resolve cloud-init first-time booting with -systemd-networkd disabled, or with requirement to add wlan/wifi units. - -* Add integration tests for cloud-init OVS/WPA first-boot - -* generate: coverage 100%, by excluding special parts, covered by the integration test - -* generate: jit starting of units only as root - -Units shall only be started JIT during early boot, by the system user -(root). If a normal user calls 'netplan generate' it shall not start the -units. Avoid asking for a password if a user (or test) executes this -command and rather fail with missing authentication. - -* Add documentation and feature flag - -* generate: no jit if network.target already started - -Do not try to enqueue new network related netplan-*.service units, if -network.target was already started. - -Co-authored-by: Lukas Märdian ---- - doc/netplan-generate.md | 5 ++ - src/generate.c | 59 +++++++++++++++++++++- - tests/integration/base.py | 12 ++++- - tests/integration/cloud-init.py | 106 ++++++++++++++++++++++++++++++++++++++++ - 4 files changed, 179 insertions(+), 3 deletions(-) - create mode 100644 tests/integration/cloud-init.py - -diff --git a/doc/netplan-generate.md b/doc/netplan-generate.md -index b325169..5ba5ee1 100644 ---- a/doc/netplan-generate.md -+++ b/doc/netplan-generate.md -@@ -25,6 +25,11 @@ 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 -diff --git a/src/generate.c b/src/generate.c -index e88dd7f..50de4dc 100644 ---- a/src/generate.c -+++ b/src/generate.c -@@ -53,6 +53,34 @@ reload_udevd(void) - 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_END -+ - static void - nd_iterator_list(gpointer value, gpointer user_data) - { -@@ -162,6 +190,8 @@ int main(int argc, char** argv) - /* 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; -+ int rc; - - /* Parse CLI options */ - opt_context = g_option_context_new(NULL); -@@ -206,8 +236,6 @@ int main(int argc, char** argv) - g_autofree char* glob_etc = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "etc/netplan/*.yaml", NULL); - g_autofree char* glob_run = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "run/netplan/*.yaml", NULL); - g_autofree char* glob_lib = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "lib/netplan/*.yaml", NULL); -- glob_t gl; -- int rc; - /* 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; -@@ -292,6 +320,33 @@ int main(int argc, char** argv) - 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_END - } - - return 0; -diff --git a/tests/integration/base.py b/tests/integration/base.py -index 4bba1e0..b4cee8e 100644 ---- a/tests/integration/base.py -+++ b/tests/integration/base.py -@@ -4,9 +4,10 @@ - # Wifi (mac80211-hwsim). These need to be run in a VM and do change the system - # configuration. - # --# Copyright (C) 2018 Canonical, Ltd. -+# Copyright (C) 2018-2020 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 -@@ -407,3 +408,12 @@ class IntegrationTestsBase(unittest.TestCase): - p = subprocess.Popen(['systemctl', 'is-active', unit], stdout=subprocess.PIPE) - out = p.communicate()[0] - return p.returncode == 0 or out.startswith(b'activating') -+ -+ -+class IntegrationTestReboot(IntegrationTestsBase): -+ -+ def tearDown(self): -+ # Do not tearDown in the middle of a reboot test -+ # Only after the 2nd part of the test ran (after reboot) -+ if os.getenv('AUTOPKGTEST_REBOOT_MARK'): -+ super().tearDown() -diff --git a/tests/integration/cloud-init.py b/tests/integration/cloud-init.py -new file mode 100644 -index 0000000..4a9910c ---- /dev/null -+++ b/tests/integration/cloud-init.py -@@ -0,0 +1,106 @@ -+#!/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) 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 sys -+import os -+import time -+import subprocess -+import unittest -+ -+from base import IntegrationTestReboot, test_backends -+ -+ -+@unittest.skipIf("networkd" not in test_backends, -+ "skipping as networkd backend tests are disabled") -+class TestSpecialReboot(IntegrationTestReboot): -+ backend = 'networkd' -+ -+ def test_generate_start_services_just_in_time(self): -+ self.setup_eth(None) -+ MARKER = 'cloud_init_generate' -+ # PART 1: set up the requried files before rebooting -+ if os.getenv('AUTOPKGTEST_REBOOT_MARK') != MARKER: -+ # any netplan YAML config -+ with open(self.config, 'w') as f: -+ f.write('''network: -+ ethernets: -+ ethbn: -+ match: {name: %(ec)s} -+ dhcp4: true''' % {'ec': self.dev_e_client}) -+ # 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' -+ with open ('/netplan-dummy.service', 'w') as f: -+ f.write('''[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 ..." -+''') -+ # A service simulating cloud-init, calling 'netplan generate' during early boot -+ # at the 'initialization' phase of systemd (before basic.target is reached). -+ with open ('/etc/systemd/system/cloud-init-dummy.service', 'w') as f: -+ f.write('''[Unit] -+Description=Simulating cloud-init's 'netplan generate' call during early boot -+DefaultDependencies=no -+Before=basic.target -+After=sysinit.target -+ -+[Install] -+WantedBy=multi-user.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/mv /netplan-dummy.service /run/systemd/system/ -+ExecStart=/usr/sbin/netplan generate -+''') -+ subprocess.check_call(['systemctl', '--quiet', 'enable', 'cloud-init-dummy.service']) -+ subprocess.check_call(['systemctl', '--quiet', 'disable', 'systemd-networkd.service']) -+ subprocess.check_call(['/tmp/autopkgtest-reboot', MARKER]) -+ # PART 2: after reboot verify all (newly created) services have been started -+ else: -+ self.addCleanup(subprocess.call, ['rm', '/run/systemd/system/netplan-dummy.service']) -+ self.addCleanup(subprocess.call, ['rm', '/etc/systemd/system/cloud-init-dummy.service']) -+ self.addCleanup(subprocess.call, ['systemctl', '--quiet', 'disable', 'cloud-init-dummy.service']) -+ -+ time.sleep(5) # Give some time for systemd to finish the boot transaction -+ # Verify our cloud-init simulation worked -+ out = subprocess.check_output(['systemctl', 'status', 'cloud-init-dummy.service'], universal_newlines=True) -+ self.assertIn('Active: active (exited)', out) -+ self.assertIn('mv /netplan-dummy.service /run/systemd/system/ (code=exited, status=0/SUCCESS)', out) -+ self.assertIn('netplan generate (code=exited, status=0/SUCCESS)', out) -+ # Verify the previously disabled networkd is running again -+ out = subprocess.check_output(['systemctl', 'status', 'systemd-networkd.service'], universal_newlines=True) -+ self.assertIn('Active: active (running)', out) -+ # Verify the newly created services were started just-in-time -+ out = subprocess.check_output(['systemctl', 'status', 'netplan-dummy.service'], universal_newlines=True) -+ self.assertIn('Active: active (exited)', out) -+ self.assertIn('echo Doing nothing ... (code=exited, status=0/SUCCESS)', out) -+ -+ -+unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2)) diff -Nru netplan.io-0.100/debian/patches/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch netplan.io-0.101/debian/patches/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch --- netplan.io-0.100/debian/patches/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/debian/patches/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch 2021-01-08 14:05:14.000000000 +0000 @@ -0,0 +1,63 @@ +From: =?utf-8?q?Lukas_M=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. +--- + 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 + '''}) diff -Nru netplan.io-0.100/debian/patches/0003-tests-tunnels-improve-WG-handshake-regex.patch netplan.io-0.101/debian/patches/0003-tests-tunnels-improve-WG-handshake-regex.patch --- netplan.io-0.100/debian/patches/0003-tests-tunnels-improve-WG-handshake-regex.patch 2020-10-19 12:49:52.000000000 +0000 +++ netplan.io-0.101/debian/patches/0003-tests-tunnels-improve-WG-handshake-regex.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -From: =?utf-8?q?Lukas_M=C3=A4rdian?= -Date: Thu, 8 Oct 2020 16:30:28 +0200 -Subject: tests:tunnels: improve WG handshake regex - ---- - tests/integration/tunnels.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/tests/integration/tunnels.py b/tests/integration/tunnels.py -index dbbe078..9299aa3 100644 ---- a/tests/integration/tunnels.py -+++ b/tests/integration/tunnels.py -@@ -137,7 +137,7 @@ class _CommonTests(): - 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') -+ 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']) - diff -Nru netplan.io-0.100/debian/patches/0004-tests-ovs-fix-OVS-timeouts.patch netplan.io-0.101/debian/patches/0004-tests-ovs-fix-OVS-timeouts.patch --- netplan.io-0.100/debian/patches/0004-tests-ovs-fix-OVS-timeouts.patch 2020-10-19 12:49:52.000000000 +0000 +++ netplan.io-0.101/debian/patches/0004-tests-ovs-fix-OVS-timeouts.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,32 +0,0 @@ -From: =?utf-8?q?Lukas_M=C3=A4rdian?= -Date: Fri, 9 Oct 2020 11:12:39 +0200 -Subject: tests:ovs: fix OVS timeouts - ---- - tests/integration/ovs.py | 5 +---- - 1 file changed, 1 insertion(+), 4 deletions(-) - -diff --git a/tests/integration/ovs.py b/tests/integration/ovs.py -index fb1cd36..9a75258 100644 ---- a/tests/integration/ovs.py -+++ b/tests/integration/ovs.py -@@ -370,18 +370,15 @@ class _CommonTests(): - %(ec)s: - addresses: [10.5.32.26/20] - gateway4: 10.5.32.1 -- match: -- macaddress: %(e_mac)s - mtu: 1500 - nameservers: - addresses: [10.5.32.99] - search: [maas] -- set-name: %(ec)s - vlans: - %(ec)s.21: - id: 21 - link: %(ec)s -- mtu: 1500''' % {'ec': self.dev_e_client, 'e_mac': self.dev_e_client_mac}) -+ mtu: 1500''' % {'ec': self.dev_e_client}) - self.generate_and_settle() - # Basic verification that the interfaces/ports are set up in OVS - out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) diff -Nru netplan.io-0.100/debian/patches/0004-tests-tunnels-improve-test-reliability.patch netplan.io-0.101/debian/patches/0004-tests-tunnels-improve-test-reliability.patch --- netplan.io-0.100/debian/patches/0004-tests-tunnels-improve-test-reliability.patch 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/debian/patches/0004-tests-tunnels-improve-test-reliability.patch 2021-01-08 14:05:14.000000000 +0000 @@ -0,0 +1,21 @@ +From: =?utf-8?q?Lukas_M=C3=A4rdian?= +Date: Fri, 8 Jan 2021 09:28:23 +0100 +Subject: tests:tunnels: improve test reliability + +--- + tests/integration/tunnels.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/tests/integration/tunnels.py b/tests/integration/tunnels.py +index 9299aa3..0bc6e58 100644 +--- a/tests/integration/tunnels.py ++++ b/tests/integration/tunnels.py +@@ -123,7 +123,7 @@ class _CommonTests(): + 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') ++ 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 diff -Nru netplan.io-0.100/debian/patches/0005-Fix-MAAS-OVS-first-boot-for-single-NIC-PXE-systems-1.patch netplan.io-0.101/debian/patches/0005-Fix-MAAS-OVS-first-boot-for-single-NIC-PXE-systems-1.patch --- netplan.io-0.100/debian/patches/0005-Fix-MAAS-OVS-first-boot-for-single-NIC-PXE-systems-1.patch 2020-10-19 12:49:52.000000000 +0000 +++ netplan.io-0.101/debian/patches/0005-Fix-MAAS-OVS-first-boot-for-single-NIC-PXE-systems-1.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -From: =?utf-8?q?Lukas_M=C3=A4rdian?= -Date: Mon, 12 Oct 2020 19:20:16 +0200 -Subject: Fix MAAS/OVS first boot for single NIC/PXE systems (#165) - -Netplan's Open vSwitch helper units are started too late, which makes systemd-networkd-wait-online.service fail the first time it is started. This leads to cloud-init init running in an invalid state, thus failing, which in turn makes the system (deployed via MAAS) end up with broken networking. - -The helper units only depend on OVSDB, which is started in network-pre.target, thus we can make them depend on ovsdb-server.service instead of openvswitch-switch.service, to avoid failures in wait-online. - -Commits: -* openvswitch: start OVS units earlier -openvswitch-switch.service runs pretty late and actually our service -units only need access to the ovs-vsctl tool, which depends on -ovsdb-server.service only. Depend on that earlier unit, to have OVS set -up before networkd starts -* ovs: do not run units Before=systemd-networkd.service ---- - src/openvswitch.c | 4 ++-- - tests/generator/base.py | 2 +- - 2 files changed, 3 insertions(+), 3 deletions(-) - -diff --git a/src/openvswitch.c b/src/openvswitch.c -index 79068d1..7fdccb0 100644 ---- a/src/openvswitch.c -+++ b/src/openvswitch.c -@@ -38,8 +38,8 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir, - 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=openvswitch-switch.service\n"); -- g_string_append_printf(s, "After=openvswitch-switch.service\n"); -+ 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); -diff --git a/tests/generator/base.py b/tests/generator/base.py -index 4a4640d..c869cf7 100644 ---- a/tests/generator/base.py -+++ b/tests/generator/base.py -@@ -47,7 +47,7 @@ ND_DHCP6_NOMTU = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv6\nLinkLocalAddressing= - ND_DHCPYES = '[Match]\nName=%s\n\n[Network]\nDHCP=yes\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=true\n' - ND_DHCPYES_NOMTU = '[Match]\nName=%s\n\n[Network]\nDHCP=yes\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=false\n' - _OVS_BASE = '[Unit]\nDescription=OpenVSwitch configuration for %(iface)s\nDefaultDependencies=no\n\ --Wants=openvswitch-switch.service\nAfter=openvswitch-switch.service\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' diff -Nru netplan.io-0.100/debian/patches/0005-tests-dbus-improve-test-stability-of-timeouts.patch netplan.io-0.101/debian/patches/0005-tests-dbus-improve-test-stability-of-timeouts.patch --- netplan.io-0.100/debian/patches/0005-tests-dbus-improve-test-stability-of-timeouts.patch 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/debian/patches/0005-tests-dbus-improve-test-stability-of-timeouts.patch 2021-01-08 14:05:14.000000000 +0000 @@ -0,0 +1,192 @@ +From: =?utf-8?q?Lukas_M=C3=A4rdian?= +Date: Fri, 8 Jan 2021 10:51:24 +0100 +Subject: tests:dbus: improve test stability of timeouts + +--- + tests/dbus/test_dbus.py | 44 ++++++++++++++++++++++++-------------------- + 1 file changed, 24 insertions(+), 20 deletions(-) + +diff --git a/tests/dbus/test_dbus.py b/tests/dbus/test_dbus.py +index b3e1d3b..2a17e0a 100644 +--- a/tests/dbus/test_dbus.py ++++ b/tests/dbus/test_dbus.py +@@ -70,7 +70,7 @@ printf '\\0' >> %(log)s + with open(self.path, "a") as fp: + fp.write("cat << EOF\n%s\nEOF" % output) + +- def set_timeout(self, timeout=1): ++ def set_timeout(self, timeout_dsec=10): + with open(self.path, "a") as fp: + fp.write(""" + if [[ "$*" == *try* ]] +@@ -78,14 +78,13 @@ then + ACTIVE=1 + trap 'ACTIVE=0' SIGUSR1 + trap 'ACTIVE=0' SIGINT +- # timeout * 10 is the specified timeout in seconds (0.1 sec sleep increments) +- while (( $ACTIVE > 0 )) && (( $ACTIVE <= $(({}*10)) )) ++ while (( $ACTIVE > 0 )) && (( $ACTIVE <= {} )) + do + ACTIVE=$(($ACTIVE+1)) + sleep 0.1 + done + fi +-""".format(timeout)) ++""".format(timeout_dsec)) + + + class TestNetplanDBus(unittest.TestCase): +@@ -131,6 +130,7 @@ class TestNetplanDBus(unittest.TestCase): + 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): +@@ -198,8 +198,8 @@ class TestNetplanDBus(unittest.TestCase): + ]) + + def test_netplan_dbus_noroot(self): +- # Process should fail instantly, if not: kill it after 1 sec +- r = subprocess.run(NETPLAN_DBUS_CMD, timeout=1, capture_output=True) ++ # 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) + +@@ -339,6 +339,8 @@ class TestNetplanDBus(unittest.TestCase): + ] + 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 +@@ -366,6 +368,7 @@ class TestNetplanDBus(unittest.TestCase): + 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 +@@ -378,7 +381,8 @@ class TestNetplanDBus(unittest.TestCase): + self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err) + + def test_netplan_dbus_config_try_cancel(self): +- self.mock_netplan_cmd.set_timeout(2) ++ # 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' +@@ -395,7 +399,7 @@ class TestNetplanDBus(unittest.TestCase): + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid), + "io.netplan.Netplan.Config", +- "Try", "u", "2", ++ "Try", "u", "3", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) +@@ -424,7 +428,7 @@ class TestNetplanDBus(unittest.TestCase): + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD2) + self.assertEqual(b'b true\n', out) +- time.sleep(1) # Give some time for the 'netplan try' process ++ 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)) +@@ -441,10 +445,10 @@ class TestNetplanDBus(unittest.TestCase): + 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=2"]]) ++ 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) # self-quit after 1 sec ++ 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' +@@ -484,14 +488,14 @@ class TestNetplanDBus(unittest.TestCase): + 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(2) ++ 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", "2", ++ "Try", "u", "3", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) +@@ -507,14 +511,14 @@ class TestNetplanDBus(unittest.TestCase): + 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(2) ++ 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", "2", ++ "Try", "u", "3", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) +@@ -525,13 +529,13 @@ class TestNetplanDBus(unittest.TestCase): + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid2), + "io.netplan.Netplan.Config", +- "Try", "u", "2", ++ "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(2) ++ self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec + cid = self._new_config_object() + BUSCTL_NETPLAN_CMD = [ + "busctl", "call", "--system", +@@ -570,7 +574,7 @@ class TestNetplanDBus(unittest.TestCase): + "io.netplan.Netplan", + "/io/netplan/Netplan/config/{}".format(cid2), + "io.netplan.Netplan.Config", +- "Try", "u", "2", ++ "Try", "u", "3", + ] + err = self._check_dbus_error(BUSCTL_NETPLAN_CMD3) + self.assertIn('This config was invalidated by another config object', err) +@@ -675,7 +679,7 @@ class TestNetplanDBus(unittest.TestCase): + ]) + + def test_netplan_dbus_config_set_uninvalidate_timeout(self): +- self.mock_netplan_cmd.set_timeout(1) ++ 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 = [ +@@ -709,7 +713,7 @@ class TestNetplanDBus(unittest.TestCase): + 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 cancel itself ++ 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) diff -Nru netplan.io-0.100/debian/patches/series netplan.io-0.101/debian/patches/series --- netplan.io-0.100/debian/patches/series 2020-10-19 12:49:52.000000000 +0000 +++ netplan.io-0.101/debian/patches/series 2021-01-08 14:05:14.000000000 +0000 @@ -1,5 +1,5 @@ -0001-Implement-just-in-time-behaviour-for-generate-162.patch 0002-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch -0003-tests-tunnels-improve-WG-handshake-regex.patch -0004-tests-ovs-fix-OVS-timeouts.patch -0005-Fix-MAAS-OVS-first-boot-for-single-NIC-PXE-systems-1.patch +0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch +0002-parse-fix-networkmanager-backend-options-for-modem-c.patch +0004-tests-tunnels-improve-test-reliability.patch +0005-tests-dbus-improve-test-stability-of-timeouts.patch diff -Nru netplan.io-0.100/doc/manpage-footer.md netplan.io-0.101/doc/manpage-footer.md --- netplan.io-0.100/doc/manpage-footer.md 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/doc/manpage-footer.md 2020-12-09 11:32:25.000000000 +0000 @@ -1,3 +1,3 @@ # SEE ALSO - **netplan-generate**(8), **netplan-apply**(8), **netplan-try**(8), **systemd-networkd**(8), **NetworkManager**(8) + **netplan-generate**(8), **netplan-apply**(8), **netplan-try**(8), **netplan-get**(8), **netplan-set**(8), **netplan-dbus**(8), **systemd-networkd**(8), **NetworkManager**(8) diff -Nru netplan.io-0.100/doc/netplan-dbus.md netplan.io-0.101/doc/netplan-dbus.md --- netplan.io-0.100/doc/netplan-dbus.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/doc/netplan-dbus.md 2020-12-09 11:32:25.000000000 +0000 @@ -0,0 +1,42 @@ +--- +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. + * ``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 -Nru netplan.io-0.100/doc/netplan-generate.md netplan.io-0.101/doc/netplan-generate.md --- netplan.io-0.100/doc/netplan-generate.md 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/doc/netplan-generate.md 2020-12-09 11:32:25.000000000 +0000 @@ -25,6 +25,11 @@ 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 diff -Nru netplan.io-0.100/doc/netplan-get.md netplan.io-0.101/doc/netplan-get.md --- netplan.io-0.100/doc/netplan-get.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/doc/netplan-get.md 2020-12-09 11:32:25.000000000 +0000 @@ -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 -Nru netplan.io-0.100/doc/netplan.md netplan.io-0.101/doc/netplan.md --- netplan.io-0.100/doc/netplan.md 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/doc/netplan.md 2020-12-09 11:32:25.000000000 +0000 @@ -275,9 +275,9 @@ ``critical`` (bool) -: (networkd backend only) Designate the connection as "critical to the - system", meaning that special care will be taken by systemd-networkd to - not release the assigned IP when the daemon is restarted. +: 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) @@ -352,7 +352,9 @@ : 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). + recognized by **``inet_pton``**(3). There should only be a single gateway + 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"`` @@ -572,6 +574,10 @@ 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. + ``routing-policy`` (mapping) : The ``routing-policy`` block defines extra routing policy for a network, @@ -1140,17 +1146,20 @@ Wireguard specific keys: - ``mark`` (scalar) – since **0.100** - : Firewall mark for outgoing WireGuard packets from this interface, - optional. +``mark`` (scalar) – since **0.100** - ``port`` (scalar) – since **0.100** - : UDP port to listen at or ``auto``. Optional, defaults to ``auto``. +: Firewall mark for outgoing WireGuard packets from this interface, + optional. - ``peers`` (sequence of mappings) – since **0.100** - : A list of peers, each having keys documented below. +``port`` (scalar) – since **0.100** - Example: +: 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: @@ -1171,35 +1180,39 @@ 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, requried 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. +``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, requried 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:`` diff -Nru netplan.io-0.100/doc/netplan-set.md netplan.io-0.101/doc/netplan-set.md --- netplan.io-0.100/doc/netplan-set.md 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/doc/netplan-set.md 2020-12-09 11:32:25.000000000 +0000 @@ -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 the name of the overwrite YAML file, e.g.: ``70-netplan-set`` => ``/etc/netplan/70-netplan-set.yaml`` + +# SEE ALSO + + **netplan**(5), **netplan-get**(8), **netplan-dbus**(8) diff -Nru netplan.io-0.100/doc/netplan-try.md netplan.io-0.101/doc/netplan-try.md --- netplan.io-0.100/doc/netplan-try.md 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/doc/netplan-try.md 2020-12-09 11:32:25.000000000 +0000 @@ -21,6 +21,9 @@ 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. diff -Nru netplan.io-0.100/examples/dbus_config_scenario.txt netplan.io-0.101/examples/dbus_config_scenario.txt --- netplan.io-0.100/examples/dbus_config_scenario.txt 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/examples/dbus_config_scenario.txt 2020-12-09 11:32:25.000000000 +0000 @@ -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 -Nru netplan.io-0.100/examples/openvswitch.yaml netplan.io-0.101/examples/openvswitch.yaml --- netplan.io-0.100/examples/openvswitch.yaml 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/examples/openvswitch.yaml 2020-12-09 11:32:25.000000000 +0000 @@ -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 -Nru netplan.io-0.100/examples/wireguard.yaml netplan.io-0.101/examples/wireguard.yaml --- netplan.io-0.100/examples/wireguard.yaml 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/examples/wireguard.yaml 2020-12-09 11:32:25.000000000 +0000 @@ -0,0 +1,27 @@ +network: + 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 diff -Nru netplan.io-0.100/.github/pull_request_template.md netplan.io-0.101/.github/pull_request_template.md --- netplan.io-0.100/.github/pull_request_template.md 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/.github/pull_request_template.md 2020-12-09 11:32:25.000000000 +0000 @@ -7,5 +7,6 @@ - [ ] 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 -Nru netplan.io-0.100/.github/workflows/build.yml netplan.io-0.101/.github/workflows/build.yml --- netplan.io-0.100/.github/workflows/build.yml 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/.github/workflows/build.yml 2020-12-09 11:32:25.000000000 +0000 @@ -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 -Nru netplan.io-0.100/.github/workflows/check-coverage.yml netplan.io-0.101/.github/workflows/check-coverage.yml --- netplan.io-0.100/.github/workflows/check-coverage.yml 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/.github/workflows/check-coverage.yml 2020-12-09 11:32:25.000000000 +0000 @@ -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: [ 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 "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 -Nru netplan.io-0.100/Makefile netplan.io-0.101/Makefile --- netplan.io-0.100/Makefile 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/Makefile 2020-12-09 11:32:25.000000000 +0000 @@ -33,21 +33,20 @@ 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 +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 util.o validation.o error.o - $(CC) -shared -Wl,-soname,libnetplan.so.$(NETPLAN_SOVER) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ `pkg-config --libs yaml-0.1` + $(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: src/generate.[hc] src/parse.[hc] src/util.[hc] src/networkd.[hc] src/nm.[hc] src/validation.[hc] src/error.[hc] 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 - $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ `pkg-config --cflags --libs libsystemd glib-2.0` +netplan-dbus: src/dbus.c src/_features.h util.o + $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ `pkg-config --cflags --libs libsystemd glib-2.0 gio-2.0` src/_features.h: src/[^_]*.[hc] printf "#include \nstatic const char *feature_flags[] __attribute__((__unused__)) = {\n" > $@ @@ -71,7 +70,7 @@ check: default linting tests/cli.py - $(NOSETESTS3) -v --with-coverage + LD_LIBRARY_PATH=. $(NOSETESTS3) -v --with-coverage tests/validate_docs.sh linting: diff -Nru netplan.io-0.100/netplan/cli/commands/apply.py netplan.io-0.101/netplan/cli/commands/apply.py --- netplan.io-0.100/netplan/cli/commands/apply.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/netplan/cli/commands/apply.py 2020-12-09 11:32:25.000000000 +0000 @@ -187,6 +187,10 @@ 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) @@ -199,9 +203,15 @@ except subprocess.CalledProcessError: logging.debug('Ignoring device without syspath: %s', device) - # apply renames to "down" devices + # 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')], @@ -234,7 +244,7 @@ interface? (bond, bridge) """ for composite in composites: - for id, settings in composite.items(): + for _, settings in composite.items(): members = settings.get('interfaces', []) for iface in members: if iface == phy: @@ -245,68 +255,49 @@ @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 "down" interfaces which can be safely - updated. + 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] - # TODO (cyphermox): factor out some of this matching code (and make it - # pretty) in its own module. - matches = {'by-driver': {}, - 'by-mac': {}, - } + # Find physical interfaces which need a rename + # But do not rename virtual interfaces for phy, settings in phys.items(): - if not settings: - continue - if phy == 'renderer': - continue + if not settings or not isinstance(settings, dict): + continue # Skip special values, like "renderer: ..." newname = settings.get('set-name') if not newname: - continue + continue # Skip if no new name needs to be set match = settings.get('match') if not match: - continue - driver = match.get('driver') - mac = match.get('macaddress') - if driver: - matches['by-driver'][driver] = newname - if mac: - matches['by-mac'][mac] = newname - - # /sys/class/net/ens3/device -> ../../../virtio0 - # /sys/class/net/ens3/device/driver -> ../../../../bus/virtio/drivers/virtio_net - for interface in interfaces: - if interface not in phys: - # do not rename virtual devices - logging.debug('Skipping non-physical interface: %s', interface) - continue - if NetplanApply.is_composite_member(composite_interfaces, interface): - logging.debug('Skipping composite member %s', interface) + 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 - - driver_name = utils.get_interface_driver_name(interface, only_down=True) - if not driver_name: - # don't allow up interfaces to match by mac + # 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 - macaddress = utils.get_interface_macaddress(interface) - if driver_name in matches['by-driver']: - new_name = matches['by-driver'][driver_name] - logging.debug(new_name) - logging.debug(interface) - if new_name != interface: - changes.update({interface: {'name': new_name}}) - if macaddress in matches['by-mac']: - new_name = matches['by-mac'][macaddress] - if new_name != interface: - changes.update({interface: {'name': new_name}}) + 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(changes) + logging.debug('Link changes: {}'.format(changes)) return changes @staticmethod diff -Nru netplan.io-0.100/netplan/cli/commands/get.py netplan.io-0.101/netplan/cli/commands/get.py --- netplan.io-0.100/netplan/cli/commands/get.py 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/netplan/cli/commands/get.py 2020-12-09 11:32:25.000000000 +0000 @@ -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.'): + 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 set command line''' + +import os +import yaml +import tempfile +import re + +import netplan.cli.utils as utils +from netplan.configmanager import ConfigManager + + +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, default='70-netplan-set', + 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 command_set(self): + if 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)) + self.write_file(set_tree, self.origin_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.'): + key = 'network.' + key + # Split at '.' but not at '\.' via negative lookbehind expression + split = re.split(r'(? # 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 @@ -24,11 +25,34 @@ 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))] + + +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 get_generator_path(): return os.environ.get('NETPLAN_GENERATE_PATH', '/lib/netplan/generate') @@ -143,24 +167,46 @@ def get_interface_macaddress(interface): # pragma: nocover (covered in autopkgtest) link = netifaces.ifaddresses(interface)[netifaces.AF_LINK][0] - return link.get('addr') -def is_interface_matching_name(interface, match_driver): - return fnmatch.fnmatchcase(interface, match_driver) +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) - - return match_driver == driver_name + # 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) - return match_mac == macaddress + # 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): diff -Nru netplan.io-0.100/netplan/configmanager.py netplan.io-0.101/netplan/configmanager.py --- netplan.io-0.100/netplan/configmanager.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/netplan/configmanager.py 2020-12-09 11:32:25.000000000 +0000 @@ -46,9 +46,11 @@ 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 @@ -56,6 +58,7 @@ def physical_interfaces(self): interfaces = {} interfaces.update(self.ethernets) + interfaces.update(self.modems) interfaces.update(self.wifis) return interfaces @@ -64,10 +67,18 @@ 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'] @@ -80,9 +91,36 @@ return self.network['bonds'] @property + def tunnels(self): + return self.network['tunnels'] + + @property def vlans(self): return self.network['vlans'] + @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 @@ -107,11 +145,16 @@ self.config['network'] = { 'ovs_ports': {}, + 'openvswitch': {}, 'ethernets': {}, + 'modems': {}, 'wifis': {}, 'bridges': {}, 'bonds': {}, - 'vlans': {} + 'tunnels': {}, + 'vlans': {}, + 'version': None, + 'renderer': None } for yaml_file in files: self._merge_yaml_config(yaml_file) @@ -119,7 +162,7 @@ for yaml_file in extra_config: self.new_interfaces |= self._merge_yaml_config(yaml_file) - logging.debug("Merged config:\n{}".format(yaml.dump(self.config, default_flow_style=False))) + 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: @@ -230,9 +273,13 @@ 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 @@ -242,9 +289,16 @@ 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 '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)) diff -Nru netplan.io-0.100/netplan/terminal.py netplan.io-0.101/netplan/terminal.py --- netplan.io-0.100/netplan/terminal.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/netplan/terminal.py 2020-12-09 11:32:25.000000000 +0000 @@ -38,16 +38,18 @@ self.save() def enable_echo(self): - attrs = termios.tcgetattr(self.fd) - attrs[3] = attrs[3] | termios.ICANON - attrs[3] = attrs[3] | termios.ECHO - termios.tcsetattr(self.fd, termios.TCSANOW, attrs) + 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): - attrs = termios.tcgetattr(self.fd) - attrs[3] = attrs[3] & ~termios.ICANON - attrs[3] = attrs[3] & ~termios.ECHO - termios.tcsetattr(self.fd, termios.TCSANOW, attrs) + 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) @@ -115,7 +117,9 @@ - dest: if set, save settings to this dict """ orig_flags = fcntl.fcntl(self.fd, fcntl.F_GETFL) - orig_term = termios.tcgetattr(self.fd) + 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}) @@ -138,7 +142,8 @@ else: orig_term = self.orig_term orig_flags = self.orig_flags - termios.tcsetattr(self.fd, termios.TCSAFLUSH, orig_term) + if sys.stdin.isatty(): + termios.tcsetattr(self.fd, termios.TCSAFLUSH, orig_term) fcntl.fcntl(self.fd, fcntl.F_SETFL, orig_flags) diff -Nru netplan.io-0.100/README.md netplan.io-0.101/README.md --- netplan.io-0.100/README.md 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/README.md 2020-12-09 11:32:25.000000000 +0000 @@ -1,8 +1,7 @@ # netplan - Backend-agnostic network configuration in YAML -[![Build Status](https://semaphoreci.com/api/v1/cyphermox/netplan/branches/master/badge.svg)](https://semaphoreci.com/cyphermox/netplan) -[![codecov](https://codecov.io/gh/CanonicalLtd/netplan/branch/master/graph/badge.svg)](https://codecov.io/gh/CanonicalLtd/netplan) -[![Snap Status](https://build.snapcraft.io/badge/CanonicalLtd/netplan.svg)](https://build.snapcraft.io/user/CanonicalLtd/netplan) +[![Build](https://github.com/CanonicalLtd/netplan/workflows/Build/badge.svg?branch=master)](https://github.com/CanonicalLtd/netplan/actions?query=branch%3Amaster+workflow%3ABuild) +[![Codecov](https://codecov.io/gh/CanonicalLtd/netplan/branch/master/graph/badge.svg)](https://codecov.io/gh/CanonicalLtd/netplan) # Website diff -Nru netplan.io-0.100/src/dbus.c netplan.io-0.101/src/dbus.c --- netplan.io-0.100/src/dbus.c 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/dbus.c 2020-12-09 11:32:25.000000000 +0000 @@ -3,47 +3,244 @@ #include #include #include +#include +#include #include #include #include +#include +#include #include +#include #include "_features.h" +#include "util.h" -// LCOV_EXCL_START -/* XXX: (cyphermox) - * This file is completely excluded from coverage on purpose. Tests should - * still include code in here, but sadly coverage does not appear to - * correctly capture tests being run over a DBus bus. +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) { +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 (getuid() != 0 && getenv("DBUS_TEST_NETPLAN_CMD") != 0) { + 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 apply: %s", err->message); - } + // 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); - } - + 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_info(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { +static int +method_info(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ sd_bus_message *reply = NULL; g_autoptr(GError) err = NULL; g_autofree gchar *stdout = NULL; @@ -52,70 +249,482 @@ exit_status = sd_bus_message_new_method_return(m, &reply); if (exit_status < 0) - return exit_status; + return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_open_container(reply, 'a', "(sv)"); if (exit_status < 0) - return exit_status; + return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_open_container(reply, 'r', "sv"); if (exit_status < 0) - return exit_status; + return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_append(reply, "s", "Features"); if (exit_status < 0) - return exit_status; + return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_open_container(reply, 'v', "as"); if (exit_status < 0) - return exit_status; + return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_append_strv(reply, (char**)feature_flags); if (exit_status < 0) - return exit_status; + return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_close_container(reply); if (exit_status < 0) - return exit_status; + return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_close_container(reply); if (exit_status < 0) - return exit_status; + return exit_status; // LCOV_EXCL_LINE exit_status = sd_bus_message_close_container(reply); if (exit_status < 0) - return exit_status; + 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 *config_delta = NULL; + char *origin_hint = NULL; + + 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); + + 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", "set", config_delta, origin, 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 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("Info", "", "a(sv)", method_info, 0), + SD_BUS_METHOD("Config", "", "o", method_config, 0), SD_BUS_VTABLE_END }; -int main(int argc, char *argv[]) { +/** + * 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_bus_add_object_vtable(bus, - &slot, - "/io/netplan/Netplan", /* object path */ - "io.netplan.Netplan", /* interface name */ - netplan_vtable, - NULL); + 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); @@ -124,28 +733,31 @@ goto finish; } - for (;;) { - r = sd_bus_process(bus, NULL); - if (r < 0) { - fprintf(stderr, "Failed to process bus: %s\n", strerror(-r)); - goto finish; - } - if (r > 0) - continue; - - /* Wait for the next request to process */ - r = sd_bus_wait(bus, (uint64_t) -1); - if (r < 0) { - fprintf(stderr, "Failed to wait on bus: %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; } - -// LCOV_EXCL_STOP diff -Nru netplan.io-0.100/src/generate.c netplan.io-0.101/src/generate.c --- netplan.io-0.100/src/generate.c 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/generate.c 2020-12-09 11:32:25.000000000 +0000 @@ -53,6 +53,34 @@ 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) { @@ -162,6 +190,7 @@ /* 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); @@ -203,39 +232,12 @@ * 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. */ - g_autofree char* glob_etc = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "etc/netplan/*.yaml", NULL); - g_autofree char* glob_run = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "run/netplan/*.yaml", NULL); - g_autofree char* glob_lib = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, "lib/netplan/*.yaml", NULL); - glob_t gl; - int rc; + if (find_yaml_glob(rootdir, &gl) != 0) + return 1; // 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; - rc = glob(glob_lib, 0, NULL, &gl); - if (rc != 0 && rc != GLOB_NOMATCH) { - // LCOV_EXCL_START - g_fprintf(stderr, "failed to glob for %s: %m\n", glob_lib); - return 1; - // LCOV_EXCL_STOP - } - - rc = glob(glob_etc, GLOB_APPEND, NULL, &gl); - if (rc != 0 && rc != GLOB_NOMATCH) { - // LCOV_EXCL_START - g_fprintf(stderr, "failed to glob for %s: %m\n", glob_etc); - return 1; - // LCOV_EXCL_STOP - } - - rc = glob(glob_run, GLOB_APPEND, NULL, &gl); - if (rc != 0 && rc != GLOB_NOMATCH) { - // LCOV_EXCL_START - g_fprintf(stderr, "failed to glob for %s: %m\n", glob_run); - return 1; - // LCOV_EXCL_STOP - } - 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]); @@ -292,6 +294,33 @@ 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 -Nru netplan.io-0.100/src/networkd.c netplan.io-0.101/src/networkd.c --- netplan.io-0.100/src/networkd.c 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/networkd.c 2020-12-09 11:32:25.000000000 +0000 @@ -86,19 +86,18 @@ g_string_append_printf(s, "Name=%s\n", def->match.original_name); } - /* Workaround for bug LP: #1804861: 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. + /* 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 or bridge. + * already and dealing with a bond, bridge or vlan. */ - if (def->bond || def->bridge) { + 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"); @@ -443,6 +442,8 @@ 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); } static void @@ -569,7 +570,7 @@ } if (def->mtubytes) { - g_string_append_printf(link, "MTUBytes=%d\n", def->mtubytes); + g_string_append_printf(link, "MTUBytes=%u\n", def->mtubytes); } if (def->emit_lldp) { diff -Nru netplan.io-0.100/src/nm.c netplan.io-0.101/src/nm.c --- netplan.io-0.100/src/nm.c 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/nm.c 2020-12-09 11:32:25.000000000 +0000 @@ -222,6 +222,7 @@ g_string_append(s, "\n"); if ( cur_route->onlink + || cur_route->mtubytes || cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC || cur_route->from) { g_string_append_printf(s, "route%d_options=", j); @@ -229,6 +230,8 @@ /* onlink for IPv6 addresses is only supported since nm-1.18.0. */ g_string_append_printf(s, "onlink=true,"); } + if (cur_route->mtubytes != NETPLAN_MTU_UNSPEC) + g_string_append_printf(s, "mtu=%u,", cur_route->mtubytes); if (cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC) g_string_append_printf(s, "table=%u,", cur_route->table); if (cur_route->from) @@ -615,7 +618,7 @@ g_string_append_printf(link_str, "cloned-mac-address=%s\n", def->set_mac); } if (def->mtubytes) { - g_string_append_printf(link_str, "mtu=%d\n", def->mtubytes); + g_string_append_printf(link_str, "mtu=%u\n", def->mtubytes); } if (def->wowlan && def->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT) g_string_append_printf(link_str, "wake-on-wlan=%u\n", def->wowlan); @@ -642,7 +645,7 @@ g_string_append_printf(link_str, "cloned-mac-address=%s\n", def->set_mac); } if (def->mtubytes) { - g_string_append_printf(link_str, "mtu=%d\n", def->mtubytes); + g_string_append_printf(link_str, "mtu=%u\n", def->mtubytes); } if (link_str->len > 0) { diff -Nru netplan.io-0.100/src/openvswitch.c netplan.io-0.101/src/openvswitch.c --- netplan.io-0.100/src/openvswitch.c 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/openvswitch.c 2020-12-09 11:32:25.000000000 +0000 @@ -38,8 +38,8 @@ 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=openvswitch-switch.service\n"); - g_string_append_printf(s, "After=openvswitch-switch.service\n"); + 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); diff -Nru netplan.io-0.100/src/parse.c netplan.io-0.101/src/parse.c --- netplan.io-0.100/src/parse.c 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/parse.c 2020-12-09 11:32:25.000000000 +0000 @@ -294,7 +294,7 @@ yaml_node_t* key, *value; const mapping_entry_handler* h; - g_assert(*error == NULL); + g_assert(error == NULL || *error == NULL); key = yaml_document_get_node(doc, entry->key); value = yaml_document_get_node(doc, entry->value); @@ -1517,6 +1517,7 @@ {"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)}, {NULL} }; @@ -2592,3 +2593,19 @@ { 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); + } + return n; +} diff -Nru netplan.io-0.100/src/parse.h netplan.io-0.101/src/parse.h --- netplan.io-0.100/src/parse.h 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/parse.h 2020-12-09 11:32:25.000000000 +0000 @@ -425,6 +425,7 @@ gboolean has_auth; } NetplanWifiAccessPoint; +#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 @@ -446,6 +447,8 @@ /* valid metrics are valid positive integers. * invalid metrics are represented by METRIC_UNSPEC */ guint metric; + + guint mtubytes; } NetplanIPRoute; typedef struct { diff -Nru netplan.io-0.100/src/util.c netplan.io-0.101/src/util.c --- netplan.io-0.100/src/util.c 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/util.c 2020-12-09 11:32:25.000000000 +0000 @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -64,7 +63,7 @@ // LCOV_EXCL_START g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message); exit(1); - // LCOV_EXCL_END + // LCOV_EXCL_STOP } } @@ -78,7 +77,7 @@ int rc; g_autofree char* rglob = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, _glob, NULL); - rc = glob(rglob, 0, NULL, &gl); + 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); @@ -88,6 +87,25 @@ 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; } /** diff -Nru netplan.io-0.100/src/util.h netplan.io-0.101/src/util.h --- netplan.io-0.100/src/util.h 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/src/util.h 2020-12-09 11:32:25.000000000 +0000 @@ -15,6 +15,8 @@ * along with this program. If not, see . */ +#define __USE_MISC +#include #pragma once extern GHashTable* wifi_frequency_24; @@ -23,6 +25,7 @@ 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); int wifi_get_freq24(int channel); int wifi_get_freq5(int channel); diff -Nru netplan.io-0.100/tests/dbus/test_dbus.py netplan.io-0.101/tests/dbus/test_dbus.py --- netplan.io-0.100/tests/dbus/test_dbus.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/dbus/test_dbus.py 2020-12-09 11:32:25.000000000 +0000 @@ -1,5 +1,5 @@ # -# Copyright (C) 2019 Canonical, Ltd. +# 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 @@ -18,6 +18,7 @@ import subprocess import tempfile import unittest +import time rootdir = os.path.dirname(os.path.dirname( os.path.dirname(os.path.abspath(__file__)))) @@ -27,6 +28,7 @@ # 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 MockCmd: @@ -64,11 +66,43 @@ 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=1): + with open(self.path, "a") as fp: + fp.write(""" +if [[ "$*" == *try* ]] +then + ACTIVE=1 + trap 'ACTIVE=0' SIGUSR1 + trap 'ACTIVE=0' SIGINT + # timeout * 10 is the specified timeout in seconds (0.1 sec sleep increments) + while (( $ACTIVE > 0 )) && (( $ACTIVE <= $(({}*10)) )) + do + ACTIVE=$(($ACTIVE+1)) + sleep 0.1 + done +fi +""".format(timeout)) + 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() @@ -94,9 +128,9 @@ 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 - p = subprocess.Popen( - os.path.join(os.path.dirname(__file__), "..", "..", "netplan-dbus"), - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + os.environ["DBUS_TEST_NETPLAN_ROOT"] = self.tmp + p = subprocess.Popen(NETPLAN_DBUS_CMD, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.addCleanup(self._cleanup_netplan_dbus, p) def _cleanup_netplan_dbus(self, p): @@ -106,6 +140,34 @@ 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"], @@ -135,6 +197,12 @@ ], ]) + def test_netplan_dbus_noroot(self): + # Process should fail instantly, if not: kill it after 1 sec + r = subprocess.run(NETPLAN_DBUS_CMD, timeout=1, 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", @@ -170,15 +238,488 @@ 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): - p = subprocess.Popen( - ["busctl", "call", - "io.netplan.Netplan", - "/io/netplan/Netplan", - "io.netplan.Netplan", - "NoSuchCommand"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) - p.wait() - self.assertEqual(p.returncode, 1) - self.assertEqual(p.stdout.read().decode("utf-8"), "") - self.assertIn("Unknown method", p.stderr.read().decode("utf-8")) + 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", "testfile", + ] + out = subprocess.check_output(BUSCTL_NETPLAN_CMD) + self.assertEqual(b'b true\n', out) + self.assertEquals(self.mock_netplan_cmd.calls(), [[ + "netplan", "set", "ethernets.eth42.dhcp6=true", + "--origin-hint=testfile", "--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) + 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"]]) + 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.mock_netplan_cmd.set_timeout(2) + 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", "2", + ] + 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 the 'netplan try' process + + # 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=2"]]) + + def test_netplan_dbus_config_try_cb(self): + self.mock_netplan_cmd.set_timeout(1) # self-quit after 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(2) + 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", "2", + ] + 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(2) + 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", "2", + ] + 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", "2", + ] + 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(2) + 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", "2", + ] + 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) + 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 cancel itself + + # 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 -Nru netplan.io-0.100/tests/generator/base.py netplan.io-0.101/tests/generator/base.py --- netplan.io-0.100/tests/generator/base.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/generator/base.py 2020-12-09 11:32:25.000000000 +0000 @@ -39,15 +39,17 @@ # 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_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=true\n' -ND_DHCP4_NOMTU = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=false\n' ND_WIFI_DHCP4 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv4\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=600\nUseMTU=true\n' -ND_DHCP6 = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv6\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=true\n' -ND_DHCP6_NOMTU = '[Match]\nName=%s\n\n[Network]\nDHCP=ipv6\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=false\n' -ND_DHCPYES = '[Match]\nName=%s\n\n[Network]\nDHCP=yes\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=true\n' -ND_DHCPYES_NOMTU = '[Match]\nName=%s\n\n[Network]\nDHCP=yes\nLinkLocalAddressing=ipv6\n\n[DHCP]\nRouteMetric=100\nUseMTU=false\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=openvswitch-switch.service\nAfter=openvswitch-switch.service\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' @@ -69,6 +71,7 @@ \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 TestBase(unittest.TestCase): diff -Nru netplan.io-0.100/tests/generator/test_errors.py netplan.io-0.101/tests/generator/test_errors.py --- netplan.io-0.100/tests/generator/test_errors.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/generator/test_errors.py 2020-12-09 11:32:25.000000000 +0000 @@ -528,6 +528,21 @@ - 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_route_family_mismatch_ipv6_to(self): self.generate('''network: version: 2 diff -Nru netplan.io-0.100/tests/generator/test_routing.py netplan.io-0.101/tests/generator/test_routing.py --- netplan.io-0.100/tests/generator/test_routing.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/generator/test_routing.py 2020-12-09 11:32:25.000000000 +0000 @@ -344,6 +344,31 @@ 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_v6_single(self): self.generate('''network: version: 2 @@ -871,6 +896,39 @@ [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({}) diff -Nru netplan.io-0.100/tests/generator/test_vlans.py netplan.io-0.101/tests/generator/test_vlans.py --- netplan.io-0.100/tests/generator/test_vlans.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/generator/test_vlans.py 2020-12-09 11:32:25.000000000 +0000 @@ -20,7 +20,7 @@ import re import unittest -from .base import TestBase +from .base import TestBase, ND_VLAN, ND_EMPTY, ND_WITHIP, ND_DHCP6_WOCARRIER class TestNetworkd(TestBase): @@ -51,20 +51,8 @@ VLAN=enred VLAN=engreen ''', - 'enblue.netdev': '''[NetDev] -Name=enblue -Kind=vlan - -[VLAN] -Id=1 -''', - 'engreen.netdev': '''[NetDev] -Name=engreen -Kind=vlan - -[VLAN] -Id=2 -''', + 'enblue.netdev': ND_VLAN % ('enblue', 1), + 'engreen.netdev': ND_VLAN % ('engreen', 2), 'enred.netdev': '''[NetDev] Name=enred MACAddress=aa:bb:cc:dd:ee:11 @@ -73,33 +61,10 @@ [VLAN] Id=3 ''', - 'enblue.network': '''[Match] -Name=enblue - -[Network] -LinkLocalAddressing=ipv6 -Address=1.2.3.4/24 -ConfigureWithoutCarrier=yes -''', - 'enred.network': '''[Match] -Name=enred - -[Network] -LinkLocalAddressing=ipv6 -ConfigureWithoutCarrier=yes -''', - 'engreen.network': '''[Match] -Name=engreen - -[Network] -DHCP=ipv6 -LinkLocalAddressing=ipv6 -ConfigureWithoutCarrier=yes + 'enblue.network': ND_WITHIP % ('enblue', '1.2.3.4/24'), + 'enred.network': ND_EMPTY % ('enred', 'ipv6'), + 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) -[DHCP] -RouteMetric=100 -UseMTU=true -'''}) self.assert_nm(None, '''[keyfile] # devices managed by networkd unmanaged-devices+=interface-name:en1,interface-name:enblue,interface-name:enred,interface-name:engreen,''') @@ -126,28 +91,54 @@ LinkLocalAddressing=ipv6 VLAN=engreen ''', - 'engreen.netdev': '''[NetDev] -Name=engreen -Kind=vlan + 'engreen.netdev': ND_VLAN % ('engreen', 2), + 'engreen.network': (ND_DHCP6_WOCARRIER % 'engreen')}) -[VLAN] -Id=2 -''', - 'engreen.network': '''[Match] -Name=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] -DHCP=ipv6 LinkLocalAddressing=ipv6 -ConfigureWithoutCarrier=yes +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)}) -[DHCP] -RouteMetric=100 -UseMTU=true -'''}) self.assert_nm(None, '''[keyfile] # devices managed by networkd -unmanaged-devices+=interface-name:en1,interface-name:enblue,interface-name:engreen,''') +unmanaged-devices+=mac:11:22:33:44:55:66,interface-name:vlan20,''') self.assert_nm_udev(None) diff -Nru netplan.io-0.100/tests/integration/base.py netplan.io-0.101/tests/integration/base.py --- netplan.io-0.100/tests/integration/base.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/integration/base.py 2020-12-09 11:32:25.000000000 +0000 @@ -4,9 +4,10 @@ # Wifi (mac80211-hwsim). These need to be run in a VM and do change the system # configuration. # -# Copyright (C) 2018 Canonical, Ltd. +# Copyright (C) 2018-2020 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 diff -Nru netplan.io-0.100/tests/integration/ethernets.py netplan.io-0.101/tests/integration/ethernets.py --- netplan.io-0.100/tests/integration/ethernets.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/integration/ethernets.py 2020-12-09 11:32:25.000000000 +0000 @@ -228,6 +228,41 @@ self.generate_and_settle() 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() + # 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() + self.assert_iface('iface1', ['inet 10.10.10.11']) + self.assert_iface_up('iface1', ['inet 10.10.10.11']) + self.assert_iface('iface2', ['inet 10.10.10.22']) + self.assert_iface_up('iface2', ['inet 10.10.10.22']) + @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") @@ -264,6 +299,47 @@ self.generate_and_settle() 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() + # 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() + # 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() + # 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") diff -Nru netplan.io-0.100/tests/integration/ovs.py netplan.io-0.101/tests/integration/ovs.py --- netplan.io-0.100/tests/integration/ovs.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/integration/ovs.py 2020-12-09 11:32:25.000000000 +0000 @@ -370,18 +370,15 @@ %(ec)s: addresses: [10.5.32.26/20] gateway4: 10.5.32.1 - match: - macaddress: %(e_mac)s mtu: 1500 nameservers: addresses: [10.5.32.99] search: [maas] - set-name: %(ec)s vlans: %(ec)s.21: id: 21 link: %(ec)s - mtu: 1500''' % {'ec': self.dev_e_client, 'e_mac': self.dev_e_client_mac}) + mtu: 1500''' % {'ec': self.dev_e_client}) self.generate_and_settle() # Basic verification that the interfaces/ports are set up in OVS out = subprocess.check_output(['ovs-vsctl', 'show'], universal_newlines=True) diff -Nru netplan.io-0.100/tests/integration/routing.py netplan.io-0.101/tests/integration/routing.py --- netplan.io-0.100/tests/integration/routing.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/integration/routing.py 2020-12-09 11:32:25.000000000 +0000 @@ -177,6 +177,23 @@ self.assertIn(b'metric 799', subprocess.check_output(['ip', '-6', 'route', 'show', '2001:f00f:f00f::/64'])) + 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.assertIn(b'mtu 777', # check mtu from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) @unittest.skipIf("networkd" not in test_backends, diff -Nru netplan.io-0.100/tests/integration/tunnels.py netplan.io-0.101/tests/integration/tunnels.py --- netplan.io-0.100/tests/integration/tunnels.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/integration/tunnels.py 2020-12-09 11:32:25.000000000 +0000 @@ -137,7 +137,7 @@ 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') + 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']) diff -Nru netplan.io-0.100/tests/test_cli_get_set.py netplan.io-0.101/tests/test_cli_get_set.py --- netplan.io-0.100/tests/test_cli_get_set.py 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.101/tests/test_cli_get_set.py 2020-12-09 11:32:25.000000000 +0000 @@ -0,0 +1,322 @@ +#!/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_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_invalid_yaml_read(self): + with open(self.path, 'w') as f: + f.write('''network: {}}''') + err = self._set(['ethernets.eth0.dhcp4=true']) + self.assertIsInstance(err, Exception) + self.assertTrue(os.path.isfile(self.path)) + self.assertIn('expected , but found \'}\'', 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]}''') + 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_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)) + + +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) diff -Nru netplan.io-0.100/tests/test_configmanager.py netplan.io-0.101/tests/test_configmanager.py --- netplan.io-0.100/tests/test_configmanager.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/test_configmanager.py 2020-12-09 11:32:25.000000000 +0000 @@ -69,6 +69,8 @@ renderer: networkd openvswitch: ports: [[patcha, patchb]] + other-config: + disable-in-band: true ethernets: eth0: dhcp4: false @@ -84,6 +86,12 @@ 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 @@ -102,6 +110,14 @@ interfaces: [ ethbond2 ] parameters: mode: 802.3ad + tunnels: + he-ipv6: + mode: sit + remote: 2.2.2.2 + local: 1.1.1.1 + addresses: + - "2001:dead:beef::2/64" + gateway6: "2001:dead:beef::1" ''', file=fd) with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'w') as fd: print("pretend .network", file=fd) @@ -117,6 +133,16 @@ self.assertNotIn('bond6', self.configmanager.physical_interfaces) self.assertNotIn('parameters', self.configmanager.bonds.get('bond5')) self.assertIn('parameters', self.configmanager.bonds.get('bond6')) + self.assertIn('wwan0', self.configmanager.modems) + self.assertIn('wwan0', self.configmanager.physical_interfaces) + self.assertIn('apn', self.configmanager.modems.get('wwan0')) + self.assertIn('he-ipv6', self.configmanager.tunnels) + self.assertNotIn('he-ipv6', self.configmanager.physical_interfaces) + self.assertIn('remote', self.configmanager.tunnels.get('he-ipv6')) + self.assertIn('other-config', self.configmanager.openvswitch) + self.assertIn('ports', self.configmanager.openvswitch) + self.assertEquals(2, self.configmanager.version) + self.assertEquals('networkd', self.configmanager.renderer) def test_parse_merging(self): self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile_merging.yaml")]) @@ -211,7 +237,7 @@ os.path.join(self.workdir.name, "etc2")) self.assertTrue(os.path.exists(os.path.join(self.workdir.name, "etc2/netplan/test.yaml"))) - @unittest.expectedFailure def test__copy_tree_missing_source(self): - self.configmanager._copy_tree(os.path.join(self.workdir.name, "nonexistent"), - os.path.join(self.workdir.name, "nonexistent2"), missing_ok=False) + 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 -Nru netplan.io-0.100/tests/test_ovs.py netplan.io-0.101/tests/test_ovs.py --- netplan.io-0.100/tests/test_ovs.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/test_ovs.py 2020-12-09 11:32:25.000000000 +0000 @@ -127,3 +127,13 @@ 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 -Nru netplan.io-0.100/tests/test_utils.py netplan.io-0.101/tests/test_utils.py --- netplan.io-0.100/tests/test_utils.py 2020-09-03 12:45:13.000000000 +0000 +++ netplan.io-0.101/tests/test_utils.py 2020-12-09 11:32:25.000000000 +0000 @@ -20,6 +20,8 @@ import tempfile import glob +from unittest.mock import patch + import netplan.cli.utils as utils @@ -71,3 +73,26 @@ 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')