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')