diff -Nru netplan.io-0.101/debian/changelog netplan.io-0.102/debian/changelog --- netplan.io-0.101/debian/changelog 2021-01-08 14:17:07.000000000 +0000 +++ netplan.io-0.102/debian/changelog 2021-03-26 12:35:37.000000000 +0000 @@ -1,3 +1,124 @@ +netplan.io (0.102-0ubuntu1~20.04.1) focal; urgency=medium + + * Backport netplan.io 0.102-0ubuntu1 to 20.04 (LP: #1919453) + - Includes NetworkManager YAML backend API + - Includes 'congestion-window', 'advertised-receive-window' & 'ttl' keys + - Includes 'netplan set' improvements + * Keep riscv64 build-time tests disabled + * Add d/p/0002-tests-tunnels-improve-flaky-wireguard-test-with-wait.patch + + -- Lukas Märdian Fri, 26 Mar 2021 13:35:37 +0100 + +netplan.io (0.102-0ubuntu1) hirsute; urgency=medium + + * New upstream release: 0.102 (LP: #1919453) + - New API for NetworkManager YAML backend + - Added congestion-window & advertised-receive-window options for routes + - Added ttl option for tunnels (LP: #1846783) + - Improved netplan set CLI to override existing files + - Moved upstream repository to https://github.com/canonical/netplan/ + - Documentation improvements + - Improved Github Actions CI and CodeQL integration + - Minor cleanup/typos/test improvements + Bug fixes: + - systemd v247 compatibility (for changing MAC address) + - OVS 2.15 compatibility (wording changes) + - Allow networkmanager: backend options for modem devices + - Prevent duplicate ARPIPTargets in NetDev files (LP: #1915837) + * Drop all distro patches, which have been integrated upstream + * Update symbols file + * Enable pristine-tar in gbp + * Allow running more tests in a container + + -- Lukas Märdian Wed, 24 Mar 2021 08:54:23 +0100 + +netplan.io (0.101-4ubuntu3) hirsute; urgency=medium + + * d/changelog: Restore history, which was lost during previous merge + * d/watch, d/copyright: Update Github URL + * d/tests/control: + - Mark ovs & cloud-init tests non-flaky + - Mark tests with the "breaks-testbed" restriction + * Fix DNS issues during tests on ppc64el (LP: #1916888): + - d/p/0007-tests-keep-management-network-up-at-all-times-during.patch + - d/p/0008-tests-integration-cleanup-OVS-WPA-files.patch + + -- Lukas Märdian Fri, 26 Feb 2021 17:47:08 +0100 + +netplan.io (0.101-4ubuntu2) hirsute; urgency=medium + + * No change rebuild with fixed ownership. + + -- Dimitri John Ledkov Tue, 16 Feb 2021 15:18:25 +0000 + +netplan.io (0.101-4ubuntu1) hirsute; urgency=medium + + * Merge with Debian. Remaining changes: + - Keep running dh_auto_test + - Keep openvswitch dependency for all arches + - 0003-tests-adopt-to-wording-changes-as-of-OVS-2.15.patch + - 0004-tests-tunnels-improve-test-reliability.patch + - 0005-tests-dbus-improve-test-stability-of-timeouts.patch + - 0006-tests-integration-adopt-for-racy-systemd-MAC-assignm.patch + + -- Lukas Märdian Mon, 08 Feb 2021 11:54:07 +0100 + +netplan.io (0.101-4) unstable; urgency=medium + + * Build-depend on ovs on amd64 only due to a bug in its postinst. + See #979366 for details. + * Drop the custom build profile, nocheck is enough. + + -- Andrej Shadura Tue, 05 Jan 2021 22:01:50 +0100 + +netplan.io (0.101-3) unstable; urgency=medium + + * Mark the package linux-any. + * Skip openvswitch-switch dependency on m68k and ppc64. + + -- Andrej Shadura Tue, 05 Jan 2021 19:28:50 +0100 + +netplan.io (0.101-2) unstable; urgency=medium + + * Reindent debian/control. + * Add build profiles. + * Add cloud tests but mark them as flaky and skip-not-installable + for now. + + -- Andrej Shadura Tue, 05 Jan 2021 17:40:42 +0100 + +netplan.io (0.101-1) unstable; urgency=medium + + [ Andrej Shadura ] + * New upstream release. + * Merge changes from Ubuntu. + * Let tests fail. + * Remove the hack to fix build with GCC 10 (actually closes: #957603). + + [ Lukas Märdian ] + * d/control: fix lintian warning about trailing whitespace + * d/p/0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch: + Fix MAC address changes with systemd v247 by using a new approach inside + systemd's .network file. It also works with older version of systemd. + * Add d/p/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch: + Allows parsing of networkmanager: backend handlers for modem devices + * Update symbols file + + [ Michael Biebl ] + * Stop using deprecated systemd-resolve tool (Closes: #979266). + + -- Andrej Shadura Mon, 04 Jan 2021 20:34:58 +0100 + +netplan.io (0.101-0ubuntu5) hirsute; urgency=medium + + * Add d/p/0004-tests-tunnels-improve-test-reliability.patch + and d/p/0005-tests-dbus-improve-test-stability-of-timeouts.patch + for improved compile-time test stability + * Add d/p/0006-tests-integration-adopt-for-racy-systemd-MAC-assignm.patch + for compatibility with new systemd + + -- Lukas Märdian Thu, 04 Feb 2021 11:35:28 +0100 + netplan.io (0.101-0ubuntu3~20.04.2) focal; urgency=medium * Backport netplan.io 0.101-0ubuntu3 to 20.04 (LP: #1908509) @@ -11,6 +132,15 @@ -- Lukas Märdian Fri, 08 Jan 2021 15:17:07 +0100 +netplan.io (0.101-0ubuntu4) hirsute; urgency=medium + + * Add d/p/0003-tests-adopt-to-wording-changes-as-of-OVS-2.15.patch: + Adopt autopkgtests to wording changes of OVS 2.15 (slave -> member) + * d/control: fix lintian warning about trailing whitespace + * d/control: fix lintian warning about deprecated d/compat file + + -- Lukas Märdian Wed, 06 Jan 2021 09:56:57 +0100 + netplan.io (0.101-0ubuntu3) hirsute; urgency=medium * Add d/p/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch: @@ -138,6 +268,46 @@ -- Lukas Märdian Thu, 03 Sep 2020 15:51:29 +0200 +netplan.io (0.99-2) experimental; urgency=medium + + * Split libnetplan off into separate packages. + * Force -fcommon to enable builds with GCC 10 to work around #957603. + + -- Andrej Shadura Mon, 27 Apr 2020 17:17:54 +0200 + +netplan.io (0.99-1) unstable; urgency=medium + + [ Andrej Shadura ] + * New upstream release. + * Drop old upstream patches. + * Update the co-maintainer list. + * Bump Standards-Version to 4.5.0. + * Update copyright years. + + [ Lukas Märdian ] + * debian:tests:control: add autopkgtest dependencies. + + -- Andrej Shadura Mon, 27 Apr 2020 11:01:26 +0200 + +netplan.io (0.99-0ubuntu6) groovy; urgency=medium + + * d/p/0005-Fix-GCC-10-fno-common-linker-errors-LP-1875412-155.patch + - Fix FTBFS if compiled using GCC-10 (LP: #1875412) + - Using upstream commit 50ac1a1 + * Refresh patches using 'gbp pq' + + -- Lukas Märdian Tue, 28 Jul 2020 14:12:28 +0200 + +netplan.io (0.99-0ubuntu5) groovy; urgency=medium + + * d/p/0001-Fix-autopkgtest-on-arm64-with-NM-1.24-146.patch + - Fix autopkgtest failure on arm64 in combination with new NM-1.24 + * d/p/0001-Call-daemon-reload-after-we-touched-systemd-unit-fil.patch + - Re-calculate systemd dependencies, after touching .service + units (LP: #1874494) + + -- Lukas Märdian Thu, 18 Jun 2020 11:36:59 +0200 + netplan.io (0.99-0ubuntu3~20.04.2) focal; urgency=medium * d/p/0002-Fix-process_link_changes-handling-up-interfaces.patch: @@ -145,6 +315,13 @@ -- Heitor Alves de Siqueira Thu, 28 May 2020 21:09:51 +0000 +netplan.io (0.99-0ubuntu4) groovy; urgency=medium + + * d/p/0002-Fix-process_link_changes-handling-up-interfaces.patch: + - Fix process_link_changes handling 'up' interfaces (LP: #1875411) + + -- Heitor Alves de Siqueira Thu, 28 May 2020 21:09:51 +0000 + netplan.io (0.99-0ubuntu3~20.04.1) focal; urgency=medium * Backport upstream bug fix for 0.99 to 20.04. (LP: #1871825) @@ -199,6 +376,28 @@ -- Łukasz 'sil2100' Zemczak Thu, 16 Apr 2020 09:13:50 +0200 +netplan.io (0.98-2) unstable; urgency=medium + + * Cherry-pick upstream commits. + * Use debhelper-compat instead of debian/compat. + * Bump debhelper from old 11 to 12. + * Bump Standards-Version to 4.4.1 (no changes). + + -- Andrej Shadura Fri, 01 Nov 2019 15:21:21 +0100 + +netplan.io (0.98-1) unstable; urgency=medium + + [ Andrej Shadura ] + * New upstream release: 0.98 (LP: #1840832). + * Run all autopkgtests with Restriction: isolation-machine (Closes: + #919426). + + [ Mathieu Trudel-Lapierre ] + * debian/control: Add Build-Depends on libsystemd-dev for the D-Bus feature, + and on dbus-x11 for dbus-launch used in tests. + + -- Andrej Shadura Thu, 26 Sep 2019 14:35:32 +0200 + netplan.io (0.98-0ubuntu4) focal; urgency=medium [ Lukas Märdian ] diff -Nru netplan.io-0.101/debian/compat netplan.io-0.102/debian/compat --- netplan.io-0.101/debian/compat 2021-01-08 09:01:54.000000000 +0000 +++ netplan.io-0.102/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -11 diff -Nru netplan.io-0.101/debian/control netplan.io-0.102/debian/control --- netplan.io-0.101/debian/control 2021-01-08 09:52:18.000000000 +0000 +++ netplan.io-0.102/debian/control 2021-03-26 12:34:42.000000000 +0000 @@ -3,11 +3,13 @@ XSBC-Original-Maintainer: Debian netplan Maintainers Uploaders: Andrej Shadura , - Mathieu Trudel-Lapierre + Mathieu Trudel-Lapierre , + Łukasz 'sil2100' Zemczak Section: net Priority: optional -Standards-Version: 4.1.3 -Build-Depends: debhelper (>= 11), +Standards-Version: 4.5.0 +Build-Depends: + debhelper-compat (= 12), pkg-config, bash-completion, libyaml-dev, @@ -20,19 +22,20 @@ libsystemd-dev, systemd, dbus-x11, - pyflakes3, - pycodestyle | pep8, - python3-nose, + pyflakes3 , + pycodestyle | pep8 , + python3-nose , pandoc, - openvswitch-switch [!riscv64], + openvswitch-switch [!riscv64] , Vcs-Git: https://salsa.debian.org/debian/netplan.io.git Vcs-Browser: https://salsa.debian.org/debian/netplan.io Homepage: https://netplan.io/ Package: netplan.io -Architecture: any +Architecture: linux-any Multi-Arch: foreign -Depends: ${shlibs:Depends}, +Depends: + ${shlibs:Depends}, ${misc:Depends}, iproute2, libnetplan0 (>= ${binary:Version}), @@ -40,7 +43,8 @@ python3-yaml, python3-netifaces, systemd (>= 239~), -Suggests: network-manager | wpasupplicant, +Suggests: + network-manager | wpasupplicant, openvswitch-switch [!riscv64], Conflicts: netplan Breaks: nplan (<< 0.34~), network-manager (<< 1.2.2-1) @@ -56,9 +60,10 @@ Currently supported backends are networkd and NetworkManager. Package: libnetplan0 -Architecture: any +Architecture: linux-any Multi-Arch: same -Depends: ${shlibs:Depends}, +Depends: + ${shlibs:Depends}, ${misc:Depends}, Description: YAML network configuration abstraction runtime library netplan reads YAML network configuration files which are written @@ -72,7 +77,7 @@ This package contains the necessary runtime library files. Package: libnetplan-dev -Architecture: any +Architecture: linux-any Multi-Arch: same Depends: ${misc:Depends}, libnetplan0 (= ${binary:Version}), @@ -87,4 +92,3 @@ . This package contains development files for developers wanting to use libnetplan in their applications. - diff -Nru netplan.io-0.101/debian/copyright netplan.io-0.102/debian/copyright --- netplan.io-0.101/debian/copyright 2020-12-11 08:20:10.000000000 +0000 +++ netplan.io-0.102/debian/copyright 2021-03-25 10:09:36.000000000 +0000 @@ -1,16 +1,16 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: netplan.io Upstream-Contact: Łukasz 'sil2100' Zemczak -Source: https://github.com/CanonicalLtd/netplan +Source: https://github.com/canonical/netplan Files: * -Copyright: 2016—2018 Canonical Ltd. +Copyright: 2016—2020 Canonical Ltd. License: GPL-3 Files: debian/* Copyright: - 2016—2018 Canonical Ltd. - 2018 Andrej Shadura + 2016—2020 Canonical Ltd. + 2018—2020 Andrej Shadura License: GPL-3 License: GPL-3 diff -Nru netplan.io-0.101/debian/gbp.conf netplan.io-0.102/debian/gbp.conf --- netplan.io-0.101/debian/gbp.conf 2021-01-08 14:05:36.000000000 +0000 +++ netplan.io-0.102/debian/gbp.conf 2021-03-25 10:09:36.000000000 +0000 @@ -1,4 +1,7 @@ [DEFAULT] -debian-branch=debian/master +debian-branch=ubuntu/master upstream-branch=upstream/latest upstream-vcs-tag=%(version)s + +[import-orig] +pristine-tar = True diff -Nru netplan.io-0.101/debian/libnetplan0.symbols netplan.io-0.102/debian/libnetplan0.symbols --- netplan.io-0.101/debian/libnetplan0.symbols 2020-12-17 11:33:20.000000000 +0000 +++ netplan.io-0.102/debian/libnetplan0.symbols 2021-03-25 10:09:36.000000000 +0000 @@ -1,7 +1,9 @@ libnetplan.so.0.0 libnetplan0 #MINVER# NETPLAN_OPTIONAL_ADDRESS_TYPES@Base 0.99 NETPLAN_WIFI_WOWLAN_TYPES@Base 0.99 + _write_netplan_conf@Base 0.102 address_option_handlers@Base 0.100 + cur_filename@Base 0.102 current_file@Base 0.99 find_yaml_glob@Base 0.101 g_string_free_to_file@Base 0.99 @@ -14,11 +16,19 @@ netdefs@Base 0.99 netdefs_ordered@Base 0.99 netplan_clear_netdefs@Base 0.101 + netplan_delete_connection@Base 0.102 netplan_finish_parse@Base 0.99 + netplan_generate@Base 0.102 + netplan_get_filename_by_id@Base 0.102 netplan_get_global_backend@Base 0.99 + netplan_get_id_from_nm_filename@Base 0.102 + netplan_netdef_new@Base 0.102 + netplan_parse_keyfile@Base 0.102 netplan_parse_yaml@Base 0.99 ovs_settings_global@Base 0.100 parser_error@Base 0.99 + process_input_file@Base 0.102 + process_yaml_hierarchy@Base 0.102 safe_mkdir_p_dir@Base 0.99 systemd_escape@Base 0.100 tunnel_mode_to_string@Base 0.99 @@ -31,4 +41,5 @@ wifi_get_freq24@Base 0.99 wifi_get_freq5@Base 0.99 wireguard_peer_handlers@Base 0.100 + write_netplan_conf@Base 0.102 yaml_error@Base 0.99 diff -Nru netplan.io-0.101/debian/patches/0000-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch netplan.io-0.102/debian/patches/0000-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch --- netplan.io-0.101/debian/patches/0000-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/debian/patches/0000-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch 2021-03-26 12:34:42.000000000 +0000 @@ -0,0 +1,202 @@ +From 6bf7ed171aaf70e3b8f369ccbd12b4059d33bafa Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Lukas=20M=C3=A4rdian?= +Date: Fri, 19 Mar 2021 17:33:59 +0100 +Subject: Disable some tests, due to ovs-vsctl missing on riscv64 + +--- + tests/generator/test_ovs.py | 25 +++++++++++++++++++++++++ + 1 file changed, 25 insertions(+) + +diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py +index e7084a9..15d4b59 100644 +--- a/tests/generator/test_ovs.py ++++ b/tests/generator/test_ovs.py +@@ -17,6 +17,9 @@ + # You should have received a copy of the GNU General Public License + # along with this program. If not, see . + ++import os ++import unittest ++ + from .base import TestBase, ND_EMPTY, ND_WITHIP, ND_DHCP4, ND_DHCP6, \ + OVS_PHYSICAL, OVS_VIRTUAL, \ + OVS_BR_EMPTY, OVS_BR_DEFAULT, \ +@@ -26,6 +29,7 @@ from .base import TestBase, ND_EMPTY, ND_WITHIP, ND_DHCP4, ND_DHCP6, \ + class TestOpenVSwitch(TestBase): + '''OVS output''' + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_interface_external_ids_other_config(self): + self.generate('''network: + version: 2 +@@ -118,6 +122,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/other-confi + # Confirm that the networkd config is still sane + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_global_set_protocols(self): + self.generate('''network: + version: 2 +@@ -161,6 +166,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 external-ids:netplan/protocols=Open + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bond_setup(self): + self.generate('''network: + version: 2 +@@ -200,6 +206,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/external-ids/if + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'bond0.network': ND_EMPTY % ('bond0', 'no')}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bond_no_bridge(self): + err = self.generate('''network: + version: 2 +@@ -213,6 +220,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/external-ids/if + ''', expect_fail=True) + self.assertIn("Bond bond0 needs to be a slave of an OpenVSwitch bridge", err) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bond_not_enough_interfaces(self): + err = self.generate('''network: + version: 2 +@@ -230,6 +238,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/external-ids/if + ''', expect_fail=True) + self.assertIn("Bond bond0 needs to have at least 2 slave interfaces", err) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bond_lacp(self): + self.generate('''network: + version: 2 +@@ -295,6 +304,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=active + ''', expect_fail=True) + self.assertIn("Key 'lacp' is only valid for interface type 'openvswitch bond'", err) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bond_mode_implicit_params(self): + self.generate('''network: + version: 2 +@@ -333,6 +343,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=balan + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'bond0.network': ND_EMPTY % ('bond0', 'no')}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bond_mode_explicit_params(self): + self.generate('''network: + version: 2 +@@ -372,6 +383,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=activ + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), + 'bond0.network': ND_EMPTY % ('bond0', 'no')}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bond_mode_ovs_invalid(self): + err = self.generate('''network: + version: 2 +@@ -392,6 +404,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=activ + ''', expect_fail=True) + self.assertIn("bond0: bond mode 'balance-rr' not supported by openvswitch", err) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bridge_setup(self): + self.generate('''network: + version: 2 +@@ -418,6 +431,7 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 + 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', + 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24')}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bridge_external_ids_other_config(self): + self.generate('''network: + version: 2 +@@ -443,6 +457,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/other-config/di + # Confirm that the bridge has been only configured for OVS + self.assert_networkd({'br0.network': ND_EMPTY % ('br0', 'ipv6')}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bridge_non_default_parameters(self): + self.generate('''network: + version: 2 +@@ -509,6 +524,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/rstp_enable=tru + ''', expect_fail=True) + self.assertIn("Key is only valid for interface type 'openvswitch bridge'", err) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bridge_set_protocols(self): + self.generate('''network: + version: 2 +@@ -550,6 +566,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/protocols=OpenF + ''', expect_fail=True) + self.assertIn("Key 'protocols' is only valid for interface type 'openvswitch bridge'", err) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bridge_controller(self): + self.generate('''network: + version: 2 +@@ -687,6 +704,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set- + # Confirm that the networkd config is still sane + self.assert_networkd({}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_missing_ssl(self): + err = self.generate('''network: + version: 2 +@@ -762,6 +780,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set- + self.assert_ovs({}) + self.assert_networkd({}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bridge_auto_ovs_backend(self): + self.generate('''network: + version: 2 +@@ -809,6 +828,7 @@ LinkLocalAddressing=no + Bond=bond0 + '''}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bond_auto_ovs_backend(self): + self.generate('''network: + version: 2 +@@ -870,6 +890,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true + 'patchy.network': ND_EMPTY % ('patchy', 'no'), + 'eth0.network': '[Match]\nName=eth0\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n'}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_patch_ports(self): + self.generate('''network: + version: 2 +@@ -918,6 +939,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true + 'patch0-1.network': ND_EMPTY % ('patch0-1', 'no'), + 'patch1-0.network': ND_EMPTY % ('patch1-0', 'no')}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_fake_vlan_bridge_setup(self): + self.generate('''network: + version: 2 +@@ -950,6 +972,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true + self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), + 'br0.100.network': ND_EMPTY % ('br0.100', 'ipv6')}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_implicit_fake_vlan_bridge_setup(self): + # Test if, when a VLAN is added to an OVS bridge, netplan will + # implicitly assume the vlan should be done via OVS as well +@@ -979,6 +1002,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true + self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), + 'br0.100.network': ND_EMPTY % ('br0.100', 'ipv6')}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_invalid_device_type(self): + err = self.generate('''network: + version: 2 +@@ -990,6 +1014,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) + self.assert_networkd({}) + ++ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') + def test_bridge_non_ovs_bond(self): + self.generate('''network: + version: 2 +-- +2.27.0 + diff -Nru netplan.io-0.101/debian/patches/0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch netplan.io-0.102/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 2021-01-08 14:05:14.000000000 +0000 +++ netplan.io-0.102/debian/patches/0001-Fix-changing-of-macaddress-with-systemd-v247-178.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,129 +0,0 @@ -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.101/debian/patches/0001-tests-bonds-fix-flaky-resend_igmp-test.patch netplan.io-0.102/debian/patches/0001-tests-bonds-fix-flaky-resend_igmp-test.patch --- netplan.io-0.101/debian/patches/0001-tests-bonds-fix-flaky-resend_igmp-test.patch 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/debian/patches/0001-tests-bonds-fix-flaky-resend_igmp-test.patch 2021-03-25 10:09:36.000000000 +0000 @@ -0,0 +1,43 @@ +From: =?utf-8?q?Lukas_M=C3=A4rdian?= +Date: Tue, 23 Mar 2021 10:59:02 +0100 +Subject: tests:bonds: fix flaky resend_igmp test + +--- + tests/integration/bonds.py | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/tests/integration/bonds.py b/tests/integration/bonds.py +index fe84f7e..072f6b0 100644 +--- a/tests/integration/bonds.py ++++ b/tests/integration/bonds.py +@@ -306,7 +306,7 @@ class _CommonTests(): + self.assertEqual(f.read().strip(), '15') + + def test_bond_resend_igmp(self): +- self.setup_eth(None) ++ self.setup_eth(None, False) + self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) + with open(self.config, 'w') as f: + f.write('''network: +@@ -318,18 +318,18 @@ class _CommonTests(): + match: {name: %(e2c)s} + bonds: + mybond: ++ addresses: [192.168.9.9/24] + interfaces: [ethbn, ethb2] + parameters: + mode: balance-rr + mii-monitor-interval: 50s + resend-igmp: 100 +- dhcp4: yes''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) ++''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() + self.assert_iface_up(self.dev_e_client, + ['master mybond'], + ['inet ']) +- self.assert_iface_up('mybond', +- ['inet 192.168.5.[0-9]+/24']) ++ self.assert_iface_up('mybond', ['inet 192.168.9.9/24']) + with open('/sys/class/net/mybond/bonding/slaves') as f: + result = f.read().strip() + self.assertIn(self.dev_e_client, result) diff -Nru netplan.io-0.101/debian/patches/0002-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch netplan.io-0.102/debian/patches/0002-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch --- netplan.io-0.101/debian/patches/0002-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch 2021-01-08 14:05:14.000000000 +0000 +++ netplan.io-0.102/debian/patches/0002-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,198 +0,0 @@ -From: =?utf-8?q?Lukas_M=C3=A4rdian?= -Date: Wed, 30 Sep 2020 15:00:20 +0200 -Subject: Disable some tests, due to ovs-vsctl missing on riscv64 - ---- - tests/generator/test_ovs.py | 25 +++++++++++++++++++++++++ - 1 file changed, 25 insertions(+) - -diff --git a/tests/generator/test_ovs.py b/tests/generator/test_ovs.py -index 601ed1c..2dacf58 100644 ---- a/tests/generator/test_ovs.py -+++ b/tests/generator/test_ovs.py -@@ -17,6 +17,9 @@ - # You should have received a copy of the GNU General Public License - # along with this program. If not, see . - -+import os -+import unittest -+ - from .base import TestBase, ND_EMPTY, ND_WITHIP, ND_DHCP4, ND_DHCP6, \ - OVS_PHYSICAL, OVS_VIRTUAL, \ - OVS_BR_EMPTY, OVS_BR_DEFAULT, \ -@@ -26,6 +29,7 @@ from .base import TestBase, ND_EMPTY, ND_WITHIP, ND_DHCP4, ND_DHCP6, \ - class TestOpenVSwitch(TestBase): - '''OVS output''' - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_interface_external_ids_other_config(self): - self.generate('''network: - version: 2 -@@ -118,6 +122,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/other-confi - # Confirm that the networkd config is still sane - self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_global_set_protocols(self): - self.generate('''network: - version: 2 -@@ -161,6 +166,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge ovs0 external-ids:netplan/protocols=Open - self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) - self.assert_networkd({'eth0.network': ND_DHCP4 % 'eth0'}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bond_setup(self): - self.generate('''network: - version: 2 -@@ -200,6 +206,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/external-ids/if - 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), - 'bond0.network': ND_EMPTY % ('bond0', 'no')}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bond_no_bridge(self): - err = self.generate('''network: - version: 2 -@@ -213,6 +220,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/external-ids/if - ''', expect_fail=True) - self.assertIn("Bond bond0 needs to be a slave of an OpenVSwitch bridge", err) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bond_not_enough_interfaces(self): - err = self.generate('''network: - version: 2 -@@ -230,6 +238,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/external-ids/if - ''', expect_fail=True) - self.assertIn("Bond bond0 needs to have at least 2 slave interfaces", err) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bond_lacp(self): - self.generate('''network: - version: 2 -@@ -295,6 +304,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/lacp=active - ''', expect_fail=True) - self.assertIn("Key 'lacp' is only valid for iterface type 'openvswitch bond'", err) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bond_mode_implicit_params(self): - self.generate('''network: - version: 2 -@@ -333,6 +343,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=balan - 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), - 'bond0.network': ND_EMPTY % ('bond0', 'no')}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bond_mode_explicit_params(self): - self.generate('''network: - version: 2 -@@ -372,6 +383,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=activ - 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24'), - 'bond0.network': ND_EMPTY % ('bond0', 'no')}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bond_mode_ovs_invalid(self): - err = self.generate('''network: - version: 2 -@@ -392,6 +404,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port bond0 external-ids:netplan/bond_mode=activ - ''', expect_fail=True) - self.assertIn("bond0: bond mode 'balance-rr' not supported by openvswitch", err) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bridge_setup(self): - self.generate('''network: - version: 2 -@@ -418,6 +431,7 @@ ExecStart=/usr/bin/ovs-vsctl --may-exist add-port br0 eth2 - 'eth2.network': '[Match]\nName=eth2\n\n[Network]\nLinkLocalAddressing=no\nBridge=br0\n', - 'br0.network': ND_WITHIP % ('br0', '192.170.1.1/24')}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bridge_external_ids_other_config(self): - self.generate('''network: - version: 2 -@@ -443,6 +457,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/other-config/di - # Confirm that the bridge has been only configured for OVS - self.assert_networkd({'br0.network': ND_EMPTY % ('br0', 'ipv6')}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bridge_non_default_parameters(self): - self.generate('''network: - version: 2 -@@ -509,6 +524,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/rstp_enable=tru - ''', expect_fail=True) - self.assertIn("Key is only valid for iterface type 'openvswitch bridge'", err) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bridge_set_protocols(self): - self.generate('''network: - version: 2 -@@ -550,6 +566,7 @@ ExecStart=/usr/bin/ovs-vsctl set Bridge br0 external-ids:netplan/protocols=OpenF - ''', expect_fail=True) - self.assertIn("Key 'protocols' is only valid for iterface type 'openvswitch bridge'", err) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bridge_controller(self): - self.generate('''network: - version: 2 -@@ -687,6 +704,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set- - # Confirm that the networkd config is still sane - self.assert_networkd({}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_missing_ssl(self): - err = self.generate('''network: - version: 2 -@@ -762,6 +780,7 @@ ExecStart=/usr/bin/ovs-vsctl set open_vswitch . external-ids:netplan/global/set- - self.assert_ovs({}) - self.assert_networkd({}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bridge_auto_ovs_backend(self): - self.generate('''network: - version: 2 -@@ -809,6 +828,7 @@ LinkLocalAddressing=no - Bond=bond0 - '''}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bond_auto_ovs_backend(self): - self.generate('''network: - version: 2 -@@ -870,6 +890,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface patchy external-ids:netplan=true - 'patchy.network': ND_EMPTY % ('patchy', 'no'), - 'eth0.network': '[Match]\nName=eth0\n\n[Network]\nLinkLocalAddressing=no\nBond=bond0\n'}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_patch_ports(self): - self.generate('''network: - version: 2 -@@ -918,6 +939,7 @@ ExecStart=/usr/bin/ovs-vsctl set Port patch1-0 external-ids:netplan=true - 'patch0-1.network': ND_EMPTY % ('patch0-1', 'no'), - 'patch1-0.network': ND_EMPTY % ('patch1-0', 'no')}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_fake_vlan_bridge_setup(self): - self.generate('''network: - version: 2 -@@ -950,6 +972,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true - self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), - 'br0.100.network': ND_EMPTY % ('br0.100', 'ipv6')}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_implicit_fake_vlan_bridge_setup(self): - # Test if, when a VLAN is added to an OVS bridge, netplan will - # implicitly assume the vlan should be done via OVS as well -@@ -979,6 +1002,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true - self.assert_networkd({'br0.network': ND_WITHIP % ('br0', '192.168.1.1/24'), - 'br0.100.network': ND_EMPTY % ('br0.100', 'ipv6')}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_invalid_device_type(self): - err = self.generate('''network: - version: 2 -@@ -990,6 +1014,7 @@ ExecStart=/usr/bin/ovs-vsctl set Interface br0.100 external-ids:netplan=true - self.assert_ovs({}) - self.assert_networkd({}) - -+ @unittest.skipIf(os.uname().machine == 'riscv64', 'missing ovs-vsctl') - def test_bridge_non_ovs_bond(self): - self.generate('''network: - version: 2 diff -Nru netplan.io-0.101/debian/patches/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch netplan.io-0.102/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 2021-01-08 14:05:14.000000000 +0000 +++ netplan.io-0.102/debian/patches/0002-parse-fix-networkmanager-backend-options-for-modem-c.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,63 +0,0 @@ -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.101/debian/patches/0002-tests-tunnels-improve-flaky-wireguard-test-with-wait.patch netplan.io-0.102/debian/patches/0002-tests-tunnels-improve-flaky-wireguard-test-with-wait.patch --- netplan.io-0.101/debian/patches/0002-tests-tunnels-improve-flaky-wireguard-test-with-wait.patch 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/debian/patches/0002-tests-tunnels-improve-flaky-wireguard-test-with-wait.patch 2021-03-26 12:35:10.000000000 +0000 @@ -0,0 +1,56 @@ +From: =?utf-8?q?Lukas_M=C3=A4rdian?= +Date: Thu, 25 Mar 2021 10:08:37 +0100 +Subject: tests:tunnels: improve flaky wireguard test with wait_output() + +--- + tests/integration/base.py | 16 ++++++++++------ + tests/integration/tunnels.py | 4 +++- + 2 files changed, 13 insertions(+), 7 deletions(-) + +diff --git a/tests/integration/base.py b/tests/integration/base.py +index c7df302..396a1b3 100644 +--- a/tests/integration/base.py ++++ b/tests/integration/base.py +@@ -333,17 +333,21 @@ class IntegrationTestsBase(unittest.TestCase): + else: + self.fail('timed out waiting for %s to get ready by NM' % iface) + +- def nm_wait_connected(self, iface, timeout): +- for t in range(timeout): ++ def wait_output(self, cmd, expected_output, timeout=10): ++ for _ in range(timeout): + try: +- out = subprocess.check_output(['nmcli', 'dev', 'show', iface]) ++ out = subprocess.check_output(cmd, universal_newlines=True) + except subprocess.CalledProcessError: +- out = b'' +- if b'(connected' in out: ++ out = '' ++ if expected_output in out: + break ++ sys.stdout.write('. ') # waiting indicator + time.sleep(1) + else: +- self.fail('timed out waiting for %s to get connected by NM:\n%s' % (iface, out.decode())) ++ self.fail('timed out waiting for "{}" to appear in {}'.format(expected_output, cmd)) ++ ++ def nm_wait_connected(self, iface, timeout): ++ self.wait_output(['nmcli', 'dev', 'show', iface], '(connected', timeout) + + @classmethod + def is_active(klass, unit): +diff --git a/tests/integration/tunnels.py b/tests/integration/tunnels.py +index 071479d..ab5d55e 100644 +--- a/tests/integration/tunnels.py ++++ b/tests/integration/tunnels.py +@@ -112,7 +112,9 @@ class _CommonTests(): + keepalive: 21 + ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) + self.generate_and_settle() +- time.sleep(2) # Give some time for handshake/connection between client & server ++ # Wait for handshake/connection between client & server ++ self.wait_output(['wg', 'show', 'wg0'], 'latest handshake') ++ self.wait_output(['wg', 'show', 'wg1'], 'latest handshake') + # Verify server + out = subprocess.check_output(['wg', 'show', 'wg0', 'private-key'], universal_newlines=True) + self.assertIn("4GgaQCy68nzNsUE5aJ9fuLzHhB65tAlwbmA72MWnOm8=", out) diff -Nru netplan.io-0.101/debian/patches/0004-tests-tunnels-improve-test-reliability.patch netplan.io-0.102/debian/patches/0004-tests-tunnels-improve-test-reliability.patch --- netplan.io-0.101/debian/patches/0004-tests-tunnels-improve-test-reliability.patch 2021-01-08 14:05:14.000000000 +0000 +++ netplan.io-0.102/debian/patches/0004-tests-tunnels-improve-test-reliability.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -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.101/debian/patches/0005-tests-dbus-improve-test-stability-of-timeouts.patch netplan.io-0.102/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 2021-01-08 14:05:14.000000000 +0000 +++ netplan.io-0.102/debian/patches/0005-tests-dbus-improve-test-stability-of-timeouts.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,192 +0,0 @@ -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.101/debian/patches/series netplan.io-0.102/debian/patches/series --- netplan.io-0.101/debian/patches/series 2021-01-08 14:05:14.000000000 +0000 +++ netplan.io-0.102/debian/patches/series 2021-03-26 12:35:10.000000000 +0000 @@ -1,5 +1,3 @@ -0002-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.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 +0000-Disable-some-tests-due-to-ovs-vsctl-missing-on-riscv.patch +0001-tests-bonds-fix-flaky-resend_igmp-test.patch +0002-tests-tunnels-improve-flaky-wireguard-test-with-wait.patch diff -Nru netplan.io-0.101/debian/rules netplan.io-0.102/debian/rules --- netplan.io-0.101/debian/rules 2020-12-11 08:20:10.000000000 +0000 +++ netplan.io-0.102/debian/rules 2021-03-25 10:09:36.000000000 +0000 @@ -1,7 +1,11 @@ #!/usr/bin/make -f +include /usr/share/dpkg/architecture.mk + %: - dh $@ --fail-missing + dh $@ override_dh_auto_install: - dh_auto_install -- LIBDIR=/usr/lib/${DEB_HOST_MULTIARCH}/ + dh_auto_install -- LIBDIR=/usr/lib/${DEB_HOST_MULTIARCH} + +.PHOHY: override_dh_auto_install diff -Nru netplan.io-0.101/debian/tests/control netplan.io-0.102/debian/tests/control --- netplan.io-0.101/debian/tests/control 2020-12-11 08:20:10.000000000 +0000 +++ netplan.io-0.102/debian/tests/control 2021-03-25 10:09:36.000000000 +0000 @@ -9,7 +9,7 @@ python3-gi, gir1.2-nm-1.0, openvswitch-switch, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-machine, skip-not-installable, breaks-testbed Features: test-name=ovs Test-Command: python3 tests/integration/run.py --test=ethernets @@ -22,7 +22,7 @@ libnm0, python3-gi, gir1.2-nm-1.0, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed Features: test-name=ethernets Test-Command: python3 tests/integration/run.py --test=bridges @@ -35,7 +35,7 @@ libnm0, python3-gi, gir1.2-nm-1.0, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed Features: test-name=bridges Test-Command: python3 tests/integration/run.py --test=bonds @@ -48,7 +48,7 @@ libnm0, python3-gi, gir1.2-nm-1.0, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed Features: test-name=bonds Test-Command: python3 tests/integration/run.py --test=routing @@ -61,7 +61,7 @@ libnm0, python3-gi, gir1.2-nm-1.0, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed Features: test-name=routing Test-Command: python3 tests/integration/run.py --test=vlans @@ -74,7 +74,7 @@ libnm0, python3-gi, gir1.2-nm-1.0, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed Features: test-name=vlans Test-Command: python3 tests/integration/run.py --test=wifi @@ -87,7 +87,7 @@ libnm0, python3-gi, gir1.2-nm-1.0, -Restrictions: allow-stderr, needs-root, isolation-machine, flaky +Restrictions: allow-stderr, needs-root, isolation-machine, flaky, breaks-testbed Features: test-name=wifi Test-Command: python3 tests/integration/run.py --test=tunnels @@ -101,7 +101,7 @@ python3-gi, gir1.2-nm-1.0, wireguard-tools, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed Features: test-name=tunnels Test-Command: python3 tests/integration/run.py --test=scenarios @@ -114,7 +114,7 @@ libnm0, python3-gi, gir1.2-nm-1.0, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed Features: test-name=scenarios Test-Command: python3 tests/integration/run.py --test=regressions @@ -127,11 +127,11 @@ libnm0, python3-gi, gir1.2-nm-1.0, -Restrictions: allow-stderr, needs-root, isolation-machine +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed Features: test-name=regressions Tests: autostart -Restrictions: allow-stderr, needs-root, isolation-container +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed Tests: cloud-init -Restrictions: allow-stderr, needs-root, isolation-container +Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed diff -Nru netplan.io-0.101/debian/watch netplan.io-0.102/debian/watch --- netplan.io-0.101/debian/watch 2020-12-11 08:20:10.000000000 +0000 +++ netplan.io-0.102/debian/watch 2021-03-25 10:09:36.000000000 +0000 @@ -1,4 +1,4 @@ version=4 opts=filenamemangle=s/.+\/v?@ANY_VERSION@@ARCHIVE_EXT@/@PACKAGE@-$1.tar.gz/,uversionmangle=s/-?rc/~rc/ \ - https://github.com/CanonicalLtd/netplan/releases .*/archive/v?@ANY_VERSION@@ARCHIVE_EXT@ + https://github.com/canonical/netplan/releases .*/archive/v?@ANY_VERSION@@ARCHIVE_EXT@ diff -Nru netplan.io-0.101/doc/netplan.md netplan.io-0.102/doc/netplan.md --- netplan.io-0.101/doc/netplan.md 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/doc/netplan.md 2021-03-16 11:38:25.000000000 +0000 @@ -133,6 +133,10 @@ : Enable wake on LAN. Off by default. + **Note:** This will not work reliably for devices matched by name + only and rendered by networkd, due to interactions with device + renaming in udev. Match devices by MAC when setting wake on LAN. + ``emit-lldp`` (bool) – since **0.99** : (networkd backend only) Whether to emit LLDP packets. Off by default. @@ -281,9 +285,10 @@ ``dhcp-identifier`` (scalar) -: When set to 'mac'; pass that setting over to systemd-networkd to use the - device's MAC address as a unique identifier rather than a RFC4361-compliant - Client ID. This has no effect when NetworkManager is used as a renderer. +: (networkd backend only) Sets the source of DHCPv4 client identifier. If ``mac`` + is specified, the MAC address of the link is used. If this option is omitted, + or if ``duid`` is specified, networkd will generate an RFC4361-compliant client + identifier for the interface by combining the link's IAID and DUID. ``dhcp4-overrides`` (mapping) @@ -578,6 +583,14 @@ : The MTU to be used for the route, in bytes. Must be a positive integer value. + ``congestion-window`` (scalar) – since **0.102** + : The congestion window to be used for the route, represented by number + of segments. Must be a positive integer value. + + ``advertised-receive-window`` (scalar) – since **0.102** + : The receive window to be advertised for the route, represented by + number of segments. Must be a positive integer value. + ``routing-policy`` (mapping) : The ``routing-policy`` block defines extra routing policy for a network, @@ -1074,6 +1087,10 @@ : Defines the address of the remote endpoint of the tunnel. +``ttl`` (scalar) – since **0.102** + +: Defines the TTL of the tunnel. + ``key`` (scalar or mapping) : Define keys to use for the tunnel. The key can be a number or a dotted @@ -1207,7 +1224,7 @@ ``public`` and ``shared`` keys. ``public`` (scalar) – since **0.100** - : A base64-encoded public key, requried for Wireguard peers. + : A base64-encoded public key, required for Wireguard peers. ``shared`` (scalar) – since **0.100** : A base64-encoded preshared key. Optional for Wireguard peers. @@ -1239,6 +1256,10 @@ link: eno1 addresses: ... +## Properties for device type ``nm-devices:`` + +The ``nm-devices`` device type is for internal use only and should not be used in normal configuration files. It enables a fallback mode for unsupported settings, using the ``passthrough`` mapping. + ## Backend-specific configuration parameters @@ -1267,6 +1288,8 @@ ``device`` (scalar) – since **0.99** : Defines the interface name for which this connection applies. + ``passthrough`` (mapping) – since **0.102** + : Can be used as a fallback mechanism to missing keyfile settings. ## Examples Configure an ethernet device with networkd, identified by its name, and enable diff -Nru netplan.io-0.101/doc/netplan-set.md netplan.io-0.102/doc/netplan-set.md --- netplan.io-0.101/doc/netplan-set.md 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/doc/netplan-set.md 2021-03-16 11:38:25.000000000 +0000 @@ -35,7 +35,7 @@ : 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`` +: Specify a name for the config file, e.g.: ``70-netplan-set`` => ``/etc/netplan/70-netplan-set.yaml`` # SEE ALSO diff -Nru netplan.io-0.101/.github/workflows/check-coverage.yml netplan.io-0.102/.github/workflows/check-coverage.yml --- netplan.io-0.101/.github/workflows/check-coverage.yml 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/.github/workflows/check-coverage.yml 2021-03-16 11:38:25.000000000 +0000 @@ -6,7 +6,7 @@ push: branches: [ master ] pull_request: - branches: [ master ] + branches: [ '**' ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: diff -Nru netplan.io-0.101/.github/workflows/codeql-analysis.yml netplan.io-0.102/.github/workflows/codeql-analysis.yml --- netplan.io-0.101/.github/workflows/codeql-analysis.yml 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/.github/workflows/codeql-analysis.yml 2021-03-16 11:38:25.000000000 +0000 @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '17 21 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'cpp', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Installs the build dependencies + - name: Install build depends + run: | + sudo sed -i '/deb-src/s/^# //' /etc/apt/sources.list + sudo apt update + sudo apt build-dep netplan.io + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff -Nru netplan.io-0.101/Makefile netplan.io-0.102/Makefile --- netplan.io-0.101/Makefile 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/Makefile 2021-03-16 11:38:25.000000000 +0000 @@ -38,15 +38,15 @@ %.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 +libnetplan.so.$(NETPLAN_SOVER): parse.o netplan.o util.o validation.o error.o parse-nm.o $(CC) -shared -Wl,-soname,libnetplan.so.$(NETPLAN_SOVER) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ `pkg-config --libs glib-2.0 gio-2.0 yaml-0.1` ln -snf libnetplan.so.$(NETPLAN_SOVER) libnetplan.so generate: libnetplan.so.$(NETPLAN_SOVER) nm.o networkd.o openvswitch.o generate.o sriov.o $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ -L. -lnetplan `pkg-config --cflags --libs glib-2.0 gio-2.0 yaml-0.1 uuid` -netplan-dbus: src/dbus.c src/_features.h util.o - $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $^ `pkg-config --cflags --libs libsystemd glib-2.0 gio-2.0` +netplan-dbus: src/dbus.c src/_features.h parse.o util.o validation.o error.o + $(CC) $(BUILDFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $(patsubst %.h,,$^) `pkg-config --cflags --libs libsystemd glib-2.0 gio-2.0 yaml-0.1` src/_features.h: src/[^_]*.[hc] printf "#include \nstatic const char *feature_flags[] __attribute__((__unused__)) = {\n" > $@ diff -Nru netplan.io-0.101/netplan/cli/commands/apply.py netplan.io-0.102/netplan/cli/commands/apply.py --- netplan.io-0.101/netplan/cli/commands/apply.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/netplan/cli/commands/apply.py 2021-03-16 11:38:25.000000000 +0000 @@ -74,6 +74,10 @@ busctl = shutil.which("busctl") if busctl is None: raise RuntimeError("missing busctl utility") + # XXX: DO NOT TOUCH or change this API call, it is used by snapd to communicate + # using core20 netplan binary/client/CLI on core18 base systems. Any change + # must be agreed upon with the snapd team, so we don't break support for + # base systems running older netplan versions. res = subprocess.call([busctl, "call", "--quiet", "--system", "io.netplan.Netplan", # the service "/io/netplan/Netplan", # the object diff -Nru netplan.io-0.101/netplan/cli/commands/get.py netplan.io-0.102/netplan/cli/commands/get.py --- netplan.io-0.101/netplan/cli/commands/get.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/netplan/cli/commands/get.py 2021-03-16 11:38:25.000000000 +0000 @@ -48,7 +48,7 @@ if self.key != 'all': # The 'network.' prefix is optional for netsted keys, its always assumed to be there - if not self.key.startswith('network.'): + if not self.key.startswith('network.') and not self.key == 'network': self.key = 'network.' + self.key # Split at '.' but not at '\.' via negative lookbehind expression for k in re.split(r'(? (str, dict): + network = set_tree.get('network', {}) + # A mapping of 'origin-hint' -> YAML tree (one subtree per netdef) + subtrees = dict() + for devtype in network: + if devtype in GLOBAL_KEYS: + continue # special handling of global keys down below + for netdef in network.get(devtype, []): + hint = FALLBACK_HINT + filename = utils.netplan_get_filename_by_id(netdef, self.root_dir) + if filename: + hint = os.path.basename(filename)[:-5] # strip prefix and .yaml + netdef_tree = {'network': {devtype: {netdef: network.get(devtype).get(netdef)}}} + # Merge all netdef trees which are going to be written to the same file/hint + subtrees[hint] = self.merge(subtrees.get(hint, {}), netdef_tree) + + # Merge GLOBAL_KEYS into one of the available subtrees + # Write to same file (if only one hint/subtree is available) + # Write to FALLBACK_HINT if multiple hints/subtrees are available, as we do not know where it is supposed to go + if any(network.get(key) for key in GLOBAL_KEYS): + # Write to the same file, if we have only one file-hint or to FALLBACK_HINT otherwise + hint = list(subtrees)[0] if len(subtrees) == 1 else FALLBACK_HINT + for key in GLOBAL_KEYS: + tree = {'network': {key: network.get(key)}} + subtrees[hint] = self.merge(subtrees.get(hint, {}), tree) + + # return a list of (str:hint, dict:subtree) tuples + return subtrees.items() + def command_set(self): - if len(self.origin_hint) == 0: + if self.origin_hint is not None and len(self.origin_hint) == 0: raise Exception('Invalid/empty origin-hint') split = self.key_value.split('=', 1) if len(split) != 2: raise Exception('Invalid value specified') key, value = split set_tree = self.parse_key(key, yaml.safe_load(value)) - self.write_file(set_tree, self.origin_hint + '.yaml', self.root_dir) + + hints = [(self.origin_hint, set_tree)] + # Override YAML config in each individual netdef file if origin-hint is not set + if self.origin_hint is None: + hints = self.split_tree_by_hint(set_tree) + + for hint, subtree in hints: + self.write_file(subtree, hint + '.yaml', self.root_dir) def parse_key(self, key, value): # The 'network.' prefix is optional for netsted keys, its always assumed to be there - if not key.startswith('network.'): + if not key.startswith('network.') and not key == 'network': key = 'network.' + key # Split at '.' but not at '\.' via negative lookbehind expression split = re.split(r'(?config_id) + 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}; + args[cur_arg] = root_dir; + cur_arg++; + } + gchar *argv[] = {SBINDIR "/" "netplan", "set", config_delta, args[0], args[1], NULL}; // for tests only: allow changing what netplan to run if (getenv("DBUS_TEST_NETPLAN_CMD") != 0) diff -Nru netplan.io-0.101/src/generate.c netplan.io-0.102/src/generate.c --- netplan.io-0.101/src/generate.c 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/src/generate.c 2021-03-16 11:38:25.000000000 +0000 @@ -171,18 +171,6 @@ return ret; } -static void -process_input_file(const char* f) -{ - GError* error = NULL; - - g_debug("Processing input file %s..", f); - if (!netplan_parse_yaml(f, &error)) { - g_fprintf(stderr, "%s\n", error->message); - exit(1); - } -} - int main(int argc, char** argv) { GError* error = NULL; @@ -225,27 +213,8 @@ if (files && !called_as_generator) { for (gchar** f = files; f && *f; ++f) process_input_file(*f); - } else { - /* Files with asciibetically higher names override/append settings from - * earlier ones (in all config dirs); files in /run/netplan/ - * shadow files in /etc/netplan/ which shadow files in /lib/netplan/. - * To do that, we put all found files in a hash table, then sort it by - * file name, and add the entries from /run after the ones from /etc - * and those after the ones from /lib. */ - if (find_yaml_glob(rootdir, &gl) != 0) - return 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; - - for (size_t i = 0; i < gl.gl_pathc; ++i) - g_hash_table_insert(configs, g_path_get_basename(gl.gl_pathv[i]), gl.gl_pathv[i]); - - config_keys = g_list_sort(g_hash_table_get_keys(configs), (GCompareFunc) strcmp); - - for (GList* i = config_keys; i != NULL; i = i->next) - process_input_file(g_hash_table_lookup(configs, i->data)); - } + } else if (!process_yaml_hierarchy(rootdir)) + return 1; // LCOV_EXCL_LINE netdefs = netplan_finish_parse(&error); if (error) { @@ -259,16 +228,15 @@ cleanup_ovs_conf(rootdir); cleanup_sriov_conf(rootdir); - if (mapping_iface && netdefs) { + if (mapping_iface && netdefs) return find_interface(mapping_iface); - } /* Generate backend specific configuration files from merged data. */ + write_ovs_conf_finish(rootdir); // OVS cleanup unit is always written if (netdefs) { g_debug("Generating output files.."); g_list_foreach (netdefs_ordered, nd_iterator_list, rootdir); write_nm_conf_finish(rootdir); - write_ovs_conf_finish(rootdir); if (any_sriov) write_sriov_conf_finish(rootdir); /* We may have written .rules & .link files, thus we must * invalidate udevd cache of its config as by default it only diff -Nru netplan.io-0.101/src/netplan.c netplan.io-0.102/src/netplan.c --- netplan.io-0.101/src/netplan.c 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/src/netplan.c 2021-03-16 11:38:25.000000000 +0000 @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "netplan.h" +#include "parse.h" + +static gboolean +write_match(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + YAML_SCALAR_PLAIN(event, emitter, "match"); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING(event, emitter, "name", def->match.original_name); + YAML_MAPPING_CLOSE(event, emitter); + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +typedef struct { + yaml_event_t* event; + yaml_emitter_t* emitter; +} _passthrough_handler_data; + +static void +_passthrough_handler(GQuark key_id, gpointer value, gpointer user_data) +{ + _passthrough_handler_data *d = user_data; + const gchar* key = g_quark_to_string(key_id); + YAML_SCALAR_PLAIN(d->event, d->emitter, key); + YAML_SCALAR_QUOTED(d->event, d->emitter, value); +error: return; // LCOV_EXCL_LINE +} + +static gboolean +write_backend_settings(yaml_event_t* event, yaml_emitter_t* emitter, NetplanBackendSettings s) { + if (s.nm.uuid || s.nm.name || s.nm.passthrough) { + YAML_SCALAR_PLAIN(event, emitter, "networkmanager"); + YAML_MAPPING_OPEN(event, emitter); + if (s.nm.uuid) { + YAML_SCALAR_PLAIN(event, emitter, "uuid"); + YAML_SCALAR_PLAIN(event, emitter, s.nm.uuid); + } + if (s.nm.name) { + YAML_SCALAR_PLAIN(event, emitter, "name"); + YAML_SCALAR_QUOTED(event, emitter, s.nm.name); + } + if (s.nm.passthrough) { + YAML_SCALAR_PLAIN(event, emitter, "passthrough"); + YAML_MAPPING_OPEN(event, emitter); + _passthrough_handler_data d; + d.event = event; + d.emitter = emitter; + g_datalist_foreach(&s.nm.passthrough, _passthrough_handler, &d); + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + } + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +static gboolean +write_access_points(yaml_event_t* event, yaml_emitter_t* emitter, const NetplanNetDefinition* def) +{ + NetplanWifiAccessPoint* ap = NULL; + GHashTableIter iter; + gpointer key, value; + YAML_SCALAR_PLAIN(event, emitter, "access-points"); + YAML_MAPPING_OPEN(event, emitter); + g_hash_table_iter_init(&iter, def->access_points); + while (g_hash_table_iter_next(&iter, &key, &value)) { + ap = value; + YAML_SCALAR_QUOTED(event, emitter, ap->ssid); + YAML_MAPPING_OPEN(event, emitter); + if (ap->hidden) { + YAML_SCALAR_PLAIN(event, emitter, "hidden"); + YAML_SCALAR_PLAIN(event, emitter, "true"); + } + YAML_SCALAR_PLAIN(event, emitter, "mode"); + if (ap->mode != NETPLAN_WIFI_MODE_OTHER) { + YAML_SCALAR_PLAIN(event, emitter, netplan_wifi_mode_to_str[ap->mode]); + } else { + // LCOV_EXCL_START + g_warning("netplan: serialize: %s (SSID %s), unsupported AP mode, falling back to 'infrastructure'", def->id, ap->ssid); + YAML_SCALAR_PLAIN(event, emitter, "infrastructure"); //TODO: add YAML comment about unsupported mode + // LCOV_EXCL_STOP + } + if (!write_backend_settings(event, emitter, ap->backend_settings)) goto error; + YAML_MAPPING_CLOSE(event, emitter); + } + YAML_MAPPING_CLOSE(event, emitter); + return TRUE; +error: return FALSE; // LCOV_EXCL_LINE +} + +/** + * Generate the Netplan YAML configuration for the selected netdef + * @def: NetplanNetDefinition (as pointer), the data to be serialized + * @rootdir: If not %NULL, generate configuration in this root directory + * (useful for testing). + */ +void +write_netplan_conf(const NetplanNetDefinition* def, const char* rootdir) +{ + g_autofree gchar *filename = NULL; + g_autofree gchar *path = NULL; + + /* NetworkManager produces one file per connection profile + * It's 90-* to be higher priority than the default 70-netplan-set.yaml */ + if (def->backend_settings.nm.uuid) + filename = g_strconcat("90-NM-", def->backend_settings.nm.uuid, ".yaml", NULL); + else + filename = g_strconcat("10-netplan-", def->id, ".yaml", NULL); + path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, "etc", "netplan", filename, NULL); + + /* Start rendering YAML output */ + yaml_emitter_t emitter_data; + yaml_event_t event_data; + yaml_emitter_t* emitter = &emitter_data; + yaml_event_t* event = &event_data; + FILE *output = fopen(path, "wb"); + + YAML_OUT_START(event, emitter, output); + /* build the netplan boilerplate YAML structure */ + YAML_SCALAR_PLAIN(event, emitter, "network"); + YAML_MAPPING_OPEN(event, emitter); + // TODO: global backend/renderer + YAML_STRING_PLAIN(event, emitter, "version", "2"); + YAML_SCALAR_PLAIN(event, emitter, netplan_def_type_to_str[def->type]); + YAML_MAPPING_OPEN(event, emitter); + YAML_SCALAR_PLAIN(event, emitter, def->id); + YAML_MAPPING_OPEN(event, emitter); + YAML_STRING_PLAIN(event, emitter, "renderer", netplan_backend_to_name[def->backend]) + + if (def->type == NETPLAN_DEF_TYPE_NM) + goto only_passthrough; //do not try to handle "unknown" connection types + + if (def->has_match) + write_match(event, emitter, def); + + /* wake-on-lan */ + if (def->wake_on_lan) + YAML_STRING_PLAIN(event, emitter, "wakeonlan", "true"); + + /* some modem settings to auto-detect GSM vs CDMA connections */ + if (def->modem_params.auto_config) + YAML_STRING_PLAIN(event, emitter, "auto-config", "true"); + YAML_STRING(event, emitter, "apn", def->modem_params.apn); + YAML_STRING(event, emitter, "device-id", def->modem_params.device_id); + YAML_STRING(event, emitter, "network-id", def->modem_params.network_id); + YAML_STRING(event, emitter, "pin", def->modem_params.pin); + YAML_STRING(event, emitter, "sim-id", def->modem_params.sim_id); + YAML_STRING(event, emitter, "sim-operator-id", def->modem_params.sim_operator_id); + + if (def->type == NETPLAN_DEF_TYPE_WIFI) + if (!write_access_points(event, emitter, def)) goto error; +only_passthrough: + if (!write_backend_settings(event, emitter, def->backend_settings)) goto error; + + /* Close remaining mappings */ + YAML_MAPPING_CLOSE(event, emitter); + YAML_MAPPING_CLOSE(event, emitter); + YAML_MAPPING_CLOSE(event, emitter); + + /* Tear down the YAML emitter */ + YAML_OUT_STOP(event, emitter); + fclose(output); + return; + + // LCOV_EXCL_START +error: + yaml_emitter_delete(emitter); + fclose(output); + // LCOV_EXCL_STOP +} + +/* XXX: implement the following functions, once needed: +void write_netplan_conf_finish(const char* rootdir) +void cleanup_netplan_conf(const char* rootdir) +*/ + +/** + * Helper function for testing only + */ +void +_write_netplan_conf(const char* netdef_id, const char* rootdir) +{ + GHashTable* ht = NULL; + const NetplanNetDefinition* def = NULL; + ht = netplan_finish_parse(NULL); + def = g_hash_table_lookup(ht, netdef_id); + write_netplan_conf(def, rootdir); +} diff -Nru netplan.io-0.101/src/netplan.h netplan.io-0.102/src/netplan.h --- netplan.io-0.101/src/netplan.h 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/src/netplan.h 2021-03-16 11:38:25.000000000 +0000 @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "parse.h" + +#define YAML_MAPPING_OPEN(event_ptr, emitter_ptr) \ +{ \ + yaml_mapping_start_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_MAP_TAG, 1, YAML_ANY_MAPPING_STYLE); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +#define YAML_MAPPING_CLOSE(event_ptr, emitter_ptr) \ +{ \ + yaml_mapping_end_event_initialize(event_ptr); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +#define YAML_SCALAR_PLAIN(event_ptr, emitter_ptr, scalar) \ +{ \ + yaml_scalar_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_STR_TAG, (yaml_char_t *)scalar, strlen(scalar), 1, 0, YAML_PLAIN_SCALAR_STYLE); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +/* Implicit plain and quoted tags, double quoted style */ +#define YAML_SCALAR_QUOTED(event_ptr, emitter_ptr, scalar) \ +{ \ + yaml_scalar_event_initialize(event_ptr, NULL, (yaml_char_t *)YAML_STR_TAG, (yaml_char_t *)scalar, strlen(scalar), 1, 1, YAML_DOUBLE_QUOTED_SCALAR_STYLE); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ +} +#define YAML_STRING(event_ptr, emitter_ptr, key, value_ptr) \ +{ \ + if (value_ptr) { \ + YAML_SCALAR_PLAIN(event, emitter, key); \ + YAML_SCALAR_QUOTED(event, emitter, value_ptr); \ + } \ +} +#define YAML_STRING_PLAIN(event_ptr, emitter_ptr, key, value_ptr) \ +{ \ + if (value_ptr) { \ + YAML_SCALAR_PLAIN(event, emitter, key); \ + YAML_SCALAR_PLAIN(event, emitter, value_ptr); \ + } \ +} +/* open YAML emitter, document, stream and initial mapping */ +#define YAML_OUT_START(event_ptr, emitter_ptr, file) \ +{ \ + yaml_emitter_initialize(emitter_ptr); \ + yaml_emitter_set_output_file(emitter_ptr, file); \ + yaml_stream_start_event_initialize(event_ptr, YAML_UTF8_ENCODING); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ + yaml_document_start_event_initialize(event_ptr, NULL, NULL, NULL, 1); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ + YAML_MAPPING_OPEN(event_ptr, emitter_ptr); \ +} +/* close initial YAML mapping, document, stream and emitter */ +#define YAML_OUT_STOP(event_ptr, emitter_ptr) \ +{ \ + YAML_MAPPING_CLOSE(event_ptr, emitter_ptr); \ + yaml_document_end_event_initialize(event_ptr, 1); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ + yaml_stream_end_event_initialize(event_ptr); \ + if (!yaml_emitter_emit(emitter_ptr, event_ptr)) goto error; \ + yaml_emitter_delete(emitter_ptr); \ +} + +static const char* const netplan_def_type_to_str[NETPLAN_DEF_TYPE_MAX_] = { + [NETPLAN_DEF_TYPE_NONE] = NULL, + [NETPLAN_DEF_TYPE_ETHERNET] = "ethernets", + [NETPLAN_DEF_TYPE_WIFI] = "wifis", + [NETPLAN_DEF_TYPE_MODEM] = "modems", + [NETPLAN_DEF_TYPE_VIRTUAL] = NULL, + [NETPLAN_DEF_TYPE_BRIDGE] = "bridges", + [NETPLAN_DEF_TYPE_BOND] = "bonds", + [NETPLAN_DEF_TYPE_VLAN] = "vlans", + [NETPLAN_DEF_TYPE_TUNNEL] = "tunnels", + [NETPLAN_DEF_TYPE_PORT] = NULL, + [NETPLAN_DEF_TYPE_NM] = "nm-devices", +}; + +void write_netplan_conf(const NetplanNetDefinition* def, const char* rootdir); diff -Nru netplan.io-0.101/src/networkd.c netplan.io-0.102/src/networkd.c --- netplan.io-0.101/src/networkd.c 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/src/networkd.c 2021-03-16 11:38:25.000000000 +0000 @@ -142,6 +142,8 @@ g_string_append_printf(params, "Mode=%s\n", tunnel_mode_to_string(def->tunnel.mode)); g_string_append_printf(params, "Local=%s\n", def->tunnel.local_ip); g_string_append_printf(params, "Remote=%s\n", def->tunnel.remote_ip); + if (def->tunnel.ttl) + g_string_append_printf(params, "TTL=%u\n", def->tunnel.ttl); if (def->tunnel.input_key) g_string_append_printf(params, "InputKey=%s\n", def->tunnel.input_key); if (def->tunnel.output_key) @@ -227,7 +229,7 @@ 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 +243,6 @@ 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"); @@ -444,6 +443,10 @@ g_string_append_printf(s, "Table=%d\n", r->table); if (r->mtubytes != NETPLAN_MTU_UNSPEC) g_string_append_printf(s, "MTUBytes=%u\n", r->mtubytes); + if (r->congestion_window != NETPLAN_CONGESTION_WINDOW_UNSPEC) + g_string_append_printf(s, "InitialCongestionWindow=%u\n", r->congestion_window); + if (r->advertised_receive_window != NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC) + g_string_append_printf(s, "InitialAdvertisedReceiveWindow=%u\n", r->advertised_receive_window); } static void @@ -569,13 +572,13 @@ } } - 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"); @@ -897,7 +900,6 @@ static void write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir) { - g_autoptr(GError) err = NULL; g_autofree gchar *stdouth = NULL; stdouth = systemd_escape(def->id); @@ -974,8 +976,8 @@ case NETPLAN_WIFI_MODE_ADHOC: g_string_append(s, " mode=1\n"); break; - case NETPLAN_WIFI_MODE_AP: - g_fprintf(stderr, "ERROR: %s: networkd does not support wifi in access point mode\n", def->id); + default: + g_fprintf(stderr, "ERROR: %s: %s: networkd does not support this wifi mode\n", def->id, ap->ssid); exit(1); } diff -Nru netplan.io-0.101/src/nm.c netplan.io-0.102/src/nm.c --- netplan.io-0.101/src/nm.c 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/src/nm.c 2021-03-16 11:38:25.000000000 +0000 @@ -1,6 +1,7 @@ /* - * Copyright (C) 2016 Canonical, Ltd. + * Copyright (C) 2016-2021 Canonical, Ltd. * Author: Martin Pitt + * Author: Lukas Märdian * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,10 +30,10 @@ #include "parse.h" #include "util.h" #include "validation.h" +#include "parse-nm.h" GString* udev_rules; - /** * Append NM device specifier of @def to @s. */ @@ -79,10 +80,13 @@ static const gboolean modem_is_gsm(const NetplanNetDefinition* def) { - if (def->type == NETPLAN_DEF_TYPE_MODEM && (def->modem_params.apn || - def->modem_params.auto_config || def->modem_params.device_id || - def->modem_params.network_id || def->modem_params.pin || - def->modem_params.sim_id || def->modem_params.sim_operator_id)) + if ( def->modem_params.apn + || def->modem_params.auto_config + || def->modem_params.device_id + || def->modem_params.network_id + || def->modem_params.pin + || def->modem_params.sim_id + || def->modem_params.sim_operator_id) return TRUE; return FALSE; @@ -115,6 +119,9 @@ if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) return "wireguard"; return "ip-tunnel"; + case NETPLAN_DEF_TYPE_NM: + /* needs to be overriden by passthrough "connection.type" setting */ + return NULL; // LCOV_EXCL_START default: g_assert_not_reached(); @@ -179,19 +186,29 @@ } static void -write_search_domains(const NetplanNetDefinition* def, GString *s) +write_search_domains(const NetplanNetDefinition* def, const char* group, GKeyFile *kf) { if (def->search_domains) { - g_string_append(s, "dns-search="); + const gchar* list[def->search_domains->len]; for (unsigned i = 0; i < def->search_domains->len; ++i) - g_string_append_printf(s, "%s;", g_array_index(def->search_domains, char*, i)); - g_string_append(s, "\n"); + list[i] = g_array_index(def->search_domains, char*, i); + g_key_file_set_string_list(kf, group, "dns-search", list, def->search_domains->len); } } static void -write_routes(const NetplanNetDefinition* def, GString *s, int family) +write_routes(const NetplanNetDefinition* def, GKeyFile *kf, int family) { + const gchar* group = NULL; + gchar* tmp_key = NULL; + GString* tmp_val = NULL; + + if (family == AF_INET) + group = "ipv4"; + else if (family == AF_INET6) + group = "ipv6"; + g_assert(group != NULL); + if (def->routes != NULL) { for (unsigned i = 0, j = 1; i < def->routes->len; ++i) { const NetplanIPRoute *cur_route = g_array_index(def->routes, NetplanIPRoute*, i); @@ -215,28 +232,41 @@ exit(1); } - g_string_append_printf(s, "route%d=%s,%s", - j, cur_route->to, cur_route->via); + tmp_key = g_strdup_printf("route%d", j); + tmp_val = g_string_new(NULL); + g_string_printf(tmp_val, "%s,%s", cur_route->to, cur_route->via); if (cur_route->metric != NETPLAN_METRIC_UNSPEC) - g_string_append_printf(s, ",%d", cur_route->metric); - g_string_append(s, "\n"); + g_string_append_printf(tmp_val, ",%d", cur_route->metric); + g_key_file_set_string(kf, group, tmp_key, tmp_val->str); + g_free(tmp_key); + g_string_free(tmp_val, TRUE); if ( cur_route->onlink + || cur_route->advertised_receive_window + || cur_route->congestion_window || cur_route->mtubytes || cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC || cur_route->from) { - g_string_append_printf(s, "route%d_options=", j); + tmp_key = g_strdup_printf("route%d_options", j); + tmp_val = g_string_new(NULL); if (cur_route->onlink) { /* onlink for IPv6 addresses is only supported since nm-1.18.0. */ - g_string_append_printf(s, "onlink=true,"); + g_string_append_printf(tmp_val, "onlink=true,"); } + if (cur_route->advertised_receive_window != NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC) + g_string_append_printf(tmp_val, "initrwnd=%u,", cur_route->advertised_receive_window); + if (cur_route->congestion_window != NETPLAN_CONGESTION_WINDOW_UNSPEC) + g_string_append_printf(tmp_val, "initcwnd=%u,", cur_route->congestion_window); if (cur_route->mtubytes != NETPLAN_MTU_UNSPEC) - g_string_append_printf(s, "mtu=%u,", cur_route->mtubytes); + g_string_append_printf(tmp_val, "mtu=%u,", cur_route->mtubytes); if (cur_route->table != NETPLAN_ROUTE_TABLE_UNSPEC) - g_string_append_printf(s, "table=%u,", cur_route->table); + g_string_append_printf(tmp_val, "table=%u,", cur_route->table); if (cur_route->from) - g_string_append_printf(s, "src=%s,", cur_route->from); - s->str[s->len - 1] = '\n'; + g_string_append_printf(tmp_val, "src=%s,", cur_route->from); + tmp_val->str[tmp_val->len - 1] = '\0'; //remove trailing comma + g_key_file_set_string(kf, group, tmp_key, tmp_val->str); + g_free(tmp_key); + g_string_free(tmp_val, TRUE); } j++; } @@ -244,100 +274,86 @@ } static void -write_bond_parameters(const NetplanNetDefinition* def, GString *s) +write_bond_parameters(const NetplanNetDefinition* def, GKeyFile *kf) { - GString* params = NULL; - - params = g_string_sized_new(200); - + GString* tmp_val = NULL; if (def->bond_params.mode) - g_string_append_printf(params, "\nmode=%s", def->bond_params.mode); + g_key_file_set_string(kf, "bond", "mode", def->bond_params.mode); if (def->bond_params.lacp_rate) - g_string_append_printf(params, "\nlacp_rate=%s", def->bond_params.lacp_rate); + g_key_file_set_string(kf, "bond", "lacp_rate", def->bond_params.lacp_rate); if (def->bond_params.monitor_interval) - g_string_append_printf(params, "\nmiimon=%s", def->bond_params.monitor_interval); + g_key_file_set_string(kf, "bond", "miimon", def->bond_params.monitor_interval); if (def->bond_params.min_links) - g_string_append_printf(params, "\nmin_links=%d", def->bond_params.min_links); + g_key_file_set_integer(kf, "bond", "min_links", def->bond_params.min_links); if (def->bond_params.transmit_hash_policy) - g_string_append_printf(params, "\nxmit_hash_policy=%s", def->bond_params.transmit_hash_policy); + g_key_file_set_string(kf, "bond", "xmit_hash_policy", def->bond_params.transmit_hash_policy); if (def->bond_params.selection_logic) - g_string_append_printf(params, "\nad_select=%s", def->bond_params.selection_logic); + g_key_file_set_string(kf, "bond", "ad_select", def->bond_params.selection_logic); if (def->bond_params.all_slaves_active) - g_string_append_printf(params, "\nall_slaves_active=%d", def->bond_params.all_slaves_active); + g_key_file_set_integer(kf, "bond", "all_slaves_active", def->bond_params.all_slaves_active); if (def->bond_params.arp_interval) - g_string_append_printf(params, "\narp_interval=%s", def->bond_params.arp_interval); + g_key_file_set_string(kf, "bond", "arp_interval", def->bond_params.arp_interval); if (def->bond_params.arp_ip_targets) { - g_string_append_printf(params, "\narp_ip_target="); + tmp_val = g_string_new(NULL); for (unsigned i = 0; i < def->bond_params.arp_ip_targets->len; ++i) { if (i > 0) - g_string_append_printf(params, ","); - g_string_append_printf(params, "%s", g_array_index(def->bond_params.arp_ip_targets, char*, i)); + g_string_append_printf(tmp_val, ","); + g_string_append_printf(tmp_val, "%s", g_array_index(def->bond_params.arp_ip_targets, char*, i)); } + g_key_file_set_string(kf, "bond", "arp_ip_target", tmp_val->str); + g_string_free(tmp_val, TRUE); } if (def->bond_params.arp_validate) - g_string_append_printf(params, "\narp_validate=%s", def->bond_params.arp_validate); + g_key_file_set_string(kf, "bond", "arp_validate", def->bond_params.arp_validate); if (def->bond_params.arp_all_targets) - g_string_append_printf(params, "\narp_all_targets=%s", def->bond_params.arp_all_targets); + g_key_file_set_string(kf, "bond", "arp_all_targets", def->bond_params.arp_all_targets); if (def->bond_params.up_delay) - g_string_append_printf(params, "\nupdelay=%s", def->bond_params.up_delay); + g_key_file_set_string(kf, "bond", "updelay", def->bond_params.up_delay); if (def->bond_params.down_delay) - g_string_append_printf(params, "\ndowndelay=%s", def->bond_params.down_delay); + g_key_file_set_string(kf, "bond", "downdelay", def->bond_params.down_delay); if (def->bond_params.fail_over_mac_policy) - g_string_append_printf(params, "\nfail_over_mac=%s", def->bond_params.fail_over_mac_policy); + g_key_file_set_string(kf, "bond", "fail_over_mac", def->bond_params.fail_over_mac_policy); if (def->bond_params.gratuitous_arp) { - g_string_append_printf(params, "\nnum_grat_arp=%d", def->bond_params.gratuitous_arp); + g_key_file_set_integer(kf, "bond", "num_grat_arp", def->bond_params.gratuitous_arp); /* Work around issue in NM where unset unsolicited_na will overwrite num_grat_arp: * https://github.com/NetworkManager/NetworkManager/commit/42b0bef33c77a0921590b2697f077e8ea7805166 */ - g_string_append_printf(params, "\nnum_unsol_na=%d", def->bond_params.gratuitous_arp); + g_key_file_set_integer(kf, "bond", "num_unsol_na", def->bond_params.gratuitous_arp); } if (def->bond_params.packets_per_slave) - g_string_append_printf(params, "\npackets_per_slave=%d", def->bond_params.packets_per_slave); + g_key_file_set_integer(kf, "bond", "packets_per_slave", def->bond_params.packets_per_slave); if (def->bond_params.primary_reselect_policy) - g_string_append_printf(params, "\nprimary_reselect=%s", def->bond_params.primary_reselect_policy); + g_key_file_set_string(kf, "bond", "primary_reselect", def->bond_params.primary_reselect_policy); if (def->bond_params.resend_igmp) - g_string_append_printf(params, "\nresend_igmp=%d", def->bond_params.resend_igmp); + g_key_file_set_integer(kf, "bond", "resend_igmp", def->bond_params.resend_igmp); if (def->bond_params.learn_interval) - g_string_append_printf(params, "\nlp_interval=%s", def->bond_params.learn_interval); + g_key_file_set_string(kf, "bond", "lp_interval", def->bond_params.learn_interval); if (def->bond_params.primary_slave) - g_string_append_printf(params, "\nprimary=%s", def->bond_params.primary_slave); - - if (params->len > 0) - g_string_append_printf(s, "\n[bond]%s\n", params->str); - - g_string_free(params, TRUE); + g_key_file_set_string(kf, "bond", "primary", def->bond_params.primary_slave); } static void -write_bridge_params(const NetplanNetDefinition* def, GString *s) +write_bridge_params(const NetplanNetDefinition* def, GKeyFile *kf) { - GString* params = NULL; - if (def->custom_bridging) { - params = g_string_sized_new(200); - if (def->bridge_params.ageing_time) - g_string_append_printf(params, "ageing-time=%s\n", def->bridge_params.ageing_time); + g_key_file_set_string(kf, "bridge", "ageing-time", def->bridge_params.ageing_time); if (def->bridge_params.priority) - g_string_append_printf(params, "priority=%u\n", def->bridge_params.priority); + g_key_file_set_uint64(kf, "bridge", "priority", def->bridge_params.priority); if (def->bridge_params.forward_delay) - g_string_append_printf(params, "forward-delay=%s\n", def->bridge_params.forward_delay); + g_key_file_set_string(kf, "bridge", "forward-delay", def->bridge_params.forward_delay); if (def->bridge_params.hello_time) - g_string_append_printf(params, "hello-time=%s\n", def->bridge_params.hello_time); + g_key_file_set_string(kf, "bridge", "hello-time", def->bridge_params.hello_time); if (def->bridge_params.max_age) - g_string_append_printf(params, "max-age=%s\n", def->bridge_params.max_age); - g_string_append_printf(params, "stp=%s\n", def->bridge_params.stp ? "true" : "false"); - - g_string_append_printf(s, "\n[bridge]\n%s", params->str); - - g_string_free(params, TRUE); + g_key_file_set_string(kf, "bridge", "max-age", def->bridge_params.max_age); + g_key_file_set_boolean(kf, "bridge", "stp", def->bridge_params.stp); } } static void -write_wireguard_params(const NetplanNetDefinition* def, GString *s) +write_wireguard_params(const NetplanNetDefinition* def, GKeyFile *kf) { + gchar* tmp_group = NULL; g_assert(def->tunnel.private_key); - g_string_append(s, "\n[wireguard]\n"); /* The key was already validated via validate_tunnel_grammar(), but we need * to differentiate between base64 key VS absolute path key-file. And a base64 @@ -347,22 +363,23 @@ g_fprintf(stderr, "%s: private key needs to be base64 encoded when using the NM backend\n", def->id); exit(1); } else - g_string_append_printf(s, "private-key=%s\n", def->tunnel.private_key); + g_key_file_set_string(kf, "wireguard", "private-key", def->tunnel.private_key); if (def->tunnel.port) - g_string_append_printf(s, "listen-port=%u\n", def->tunnel.port); + g_key_file_set_uint64(kf, "wireguard", "listen-port", def->tunnel.port); if (def->tunnel.fwmark) - g_string_append_printf(s, "fwmark=%u\n", def->tunnel.fwmark); + g_key_file_set_uint64(kf, "wireguard", "fwmark", def->tunnel.fwmark); for (guint i = 0; i < def->wireguard_peers->len; i++) { NetplanWireguardPeer *peer = g_array_index (def->wireguard_peers, NetplanWireguardPeer*, i); g_assert(peer->public_key); - g_string_append_printf(s, "\n[wireguard-peer.%s]\n", peer->public_key); + tmp_group = g_strdup_printf("wireguard-peer.%s", peer->public_key); if (peer->keepalive) - g_string_append_printf(s, "persistent-keepalive=%d\n", peer->keepalive); + g_key_file_set_integer(kf, tmp_group, "persistent-keepalive", peer->keepalive); if (peer->endpoint) - g_string_append_printf(s, "endpoint=%s\n", peer->endpoint); + g_key_file_set_string(kf, tmp_group, "endpoint", peer->endpoint); + /* The key was already validated via validate_tunnel_grammar(), but we need * to differentiate between base64 key VS absolute path key-file. And a base64 * string could (theoretically) start with '/', so we use is_wireguard_key() @@ -372,111 +389,93 @@ g_fprintf(stderr, "%s: shared key needs to be base64 encoded when using the NM backend\n", def->id); exit(1); } else { - g_string_append_printf(s, "preshared-key=%s\n", peer->preshared_key); - g_string_append(s, "preshared-key-flags=0\n"); + g_key_file_set_value(kf, tmp_group, "preshared-key", peer->preshared_key); + g_key_file_set_uint64(kf, tmp_group, "preshared-key-flags", 0); } } if (peer->allowed_ips && peer->allowed_ips->len > 0) { - g_string_append(s, "allowed-ips="); - for (guint i = 0; i < peer->allowed_ips->len; ++i) { - if (i > 0 ) g_string_append_c(s, ';'); - g_string_append_printf(s, "%s", g_array_index(peer->allowed_ips, char*, i)); - } - g_string_append_c(s, '\n'); + const gchar* list[peer->allowed_ips->len]; + for (guint j = 0; j < peer->allowed_ips->len; ++j) + list[j] = g_array_index(peer->allowed_ips, char*, j); + g_key_file_set_string_list(kf, tmp_group, "allowed-ips", list, peer->allowed_ips->len); } + g_free(tmp_group); } } static void -write_tunnel_params(const NetplanNetDefinition* def, GString *s) +write_tunnel_params(const NetplanNetDefinition* def, GKeyFile *kf) { - g_string_append(s, "\n[ip-tunnel]\n"); - - g_string_append_printf(s, "mode=%d\n", def->tunnel.mode); - g_string_append_printf(s, "local=%s\n", def->tunnel.local_ip); - g_string_append_printf(s, "remote=%s\n", def->tunnel.remote_ip); - + g_key_file_set_integer(kf, "ip-tunnel", "mode", def->tunnel.mode); + g_key_file_set_string(kf, "ip-tunnel", "local", def->tunnel.local_ip); + g_key_file_set_string(kf, "ip-tunnel", "remote", def->tunnel.remote_ip); + if (def->tunnel.ttl) + g_key_file_set_uint64(kf, "ip-tunnel", "ttl", def->tunnel.ttl); if (def->tunnel.input_key) - g_string_append_printf(s, "input-key=%s\n", def->tunnel.input_key); + g_key_file_set_string(kf, "ip-tunnel", "input-key", def->tunnel.input_key); if (def->tunnel.output_key) - g_string_append_printf(s, "output-key=%s\n", def->tunnel.output_key); + g_key_file_set_string(kf, "ip-tunnel", "output-key", def->tunnel.output_key); } static void -write_dot1x_auth_parameters(const NetplanAuthenticationSettings* auth, GString *s) +write_dot1x_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) { - if (auth->eap_method == NETPLAN_AUTH_EAP_NONE) { + if (auth->eap_method == NETPLAN_AUTH_EAP_NONE) return; - } - - g_string_append_printf(s, "\n[802-1x]\n"); switch (auth->eap_method) { case NETPLAN_AUTH_EAP_NONE: break; // LCOV_EXCL_LINE case NETPLAN_AUTH_EAP_TLS: - g_string_append(s, "eap=tls\n"); + g_key_file_set_string(kf, "802-1x", "eap", "tls"); break; case NETPLAN_AUTH_EAP_PEAP: - g_string_append(s, "eap=peap\n"); + g_key_file_set_string(kf, "802-1x", "eap", "peap"); break; case NETPLAN_AUTH_EAP_TTLS: - g_string_append(s, "eap=ttls\n"); + g_key_file_set_string(kf, "802-1x", "eap", "ttls"); break; } - if (auth->identity) { - g_string_append_printf(s, "identity=%s\n", auth->identity); - } - if (auth->anonymous_identity) { - g_string_append_printf(s, "anonymous-identity=%s\n", auth->anonymous_identity); - } - if (auth->password && auth->key_management != NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK) { - g_string_append_printf(s, "password=%s\n", auth->password); - } - if (auth->ca_certificate) { - g_string_append_printf(s, "ca-cert=%s\n", auth->ca_certificate); - } - if (auth->client_certificate) { - g_string_append_printf(s, "client-cert=%s\n", auth->client_certificate); - } - if (auth->client_key) { - g_string_append_printf(s, "private-key=%s\n", auth->client_key); - } - if (auth->client_key_password) { - g_string_append_printf(s, "private-key-password=%s\n", auth->client_key_password); - } - if (auth->phase2_auth) { - g_string_append_printf(s, "phase2-auth=%s\n", auth->phase2_auth); - } - + if (auth->identity) + g_key_file_set_string(kf, "802-1x", "identity", auth->identity); + if (auth->anonymous_identity) + g_key_file_set_string(kf, "802-1x", "anonymous-identity", auth->anonymous_identity); + if (auth->password && auth->key_management != NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK) + g_key_file_set_string(kf, "802-1x", "password", auth->password); + if (auth->ca_certificate) + g_key_file_set_string(kf, "802-1x", "ca-cert", auth->ca_certificate); + if (auth->client_certificate) + g_key_file_set_string(kf, "802-1x", "client-cert", auth->client_certificate); + if (auth->client_key) + g_key_file_set_string(kf, "802-1x", "private-key", auth->client_key); + if (auth->client_key_password) + g_key_file_set_string(kf, "802-1x", "private-key-password", auth->client_key_password); + if (auth->phase2_auth) + g_key_file_set_string(kf, "802-1x", "phase2-auth", auth->phase2_auth); } static void -write_wifi_auth_parameters(const NetplanAuthenticationSettings* auth, GString *s) +write_wifi_auth_parameters(const NetplanAuthenticationSettings* auth, GKeyFile *kf) { - if (auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_NONE) { + if (auth->key_management == NETPLAN_AUTH_KEY_MANAGEMENT_NONE) return; - } - - g_string_append(s, "\n[wifi-security]\n"); switch (auth->key_management) { case NETPLAN_AUTH_KEY_MANAGEMENT_NONE: break; // LCOV_EXCL_LINE case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_PSK: - g_string_append(s, "key-mgmt=wpa-psk\n"); - if (auth->password) { - g_string_append_printf(s, "psk=%s\n", auth->password); - } + g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-psk"); + if (auth->password) + g_key_file_set_string(kf, "wifi-security", "psk", auth->password); break; case NETPLAN_AUTH_KEY_MANAGEMENT_WPA_EAP: - g_string_append(s, "key-mgmt=wpa-eap\n"); + g_key_file_set_string(kf, "wifi-security", "key-mgmt", "wpa-eap"); break; case NETPLAN_AUTH_KEY_MANAGEMENT_8021X: - g_string_append(s, "key-mgmt=ieee8021x\n"); + g_key_file_set_string(kf, "wifi-security", "key-mgmt", "ieee8021x"); break; } - write_dot1x_auth_parameters(auth, s); + write_dot1x_auth_parameters(auth, kf); } static void @@ -487,6 +486,52 @@ } /** + * Special handling for passthrough mode: read key-value pairs from + * "backend_settings.nm.passthrough" and inject them into the keyfile as-is. + */ +static void +write_fallback_key_value(GQuark key_id, gpointer value, gpointer user_data) +{ + GKeyFile *kf = user_data; + gchar* val = value; + /* Group name may contain dots, but key name may not. + * The "tc" group is a special case, where it is the other way around, e.g.: + * tc->qdisc.root + * tc->tfilter.ffff: */ + const gchar* key = g_quark_to_string(key_id); + gchar **group_key = g_strsplit(key, ".", -1); + guint len = g_strv_length(group_key); + g_autofree gchar* old_key = NULL; + gboolean has_key = FALSE; + g_autofree gchar* k = NULL; + g_autofree gchar* group = NULL; + if (!g_strcmp0(group_key[0], "tc") && len > 2) { + k = g_strconcat(group_key[1], ".", group_key[2], NULL); + group = g_strdup(group_key[0]); + } else { + k = group_key[len-1]; + group_key[len-1] = NULL; //remove key from array + group = g_strjoinv(".", group_key); //re-combine group parts + } + + has_key = g_key_file_has_key(kf, group, k, NULL); + old_key = g_key_file_get_string(kf, group, k, NULL); + g_key_file_set_string(kf, group, k, val); + /* delete the dummy key, if this was just an empty group */ + if (!g_strcmp0(k, NETPLAN_NM_EMPTY_GROUP)) + g_key_file_remove_key(kf, group, k, NULL); + else if (!has_key) { + g_debug("NetworkManager: passing through fallback key: %s.%s=%s", group, k, val); + g_key_file_set_comment(kf, group, k, "Netplan: passthrough setting", NULL); + } else if (!!g_strcmp0(val, old_key)) { + g_debug("NetworkManager: fallback override: %s.%s=%s", group, k, val); + g_key_file_set_comment(kf, group, k, "Netplan: passthrough override", NULL); + } + + g_strfreev(group_key); +} + +/** * Generate NetworkManager configuration in @rootdir/run/NetworkManager/ for a * particular NetplanNetDefinition and NetplanWifiAccessPoint, as NM requires a separate * connection file for each SSID. @@ -499,8 +544,13 @@ static void write_nm_conf_access_point(NetplanNetDefinition* def, const char* rootdir, const NetplanWifiAccessPoint* ap) { - GString *s = NULL; - g_autofree char* conf_path = NULL; + g_autoptr(GKeyFile) kf = NULL; + g_autoptr(GError) error = NULL; + g_autofree gchar* conf_path = NULL; + g_autofree gchar* full_path = NULL; + g_autofree gchar* nd_nm_id = NULL; + const gchar* nm_type = NULL; + gchar* tmp_key = NULL; mode_t orig_umask; char uuidstr[37]; const char *match_interface_name = NULL; @@ -515,19 +565,35 @@ return; } - s = g_string_new(NULL); - g_string_append_printf(s, "[connection]\nid=netplan-%s", def->id); - if (ap) - g_string_append_printf(s, "-%s", ap->ssid); - g_string_append_printf(s, "\ntype=%s\n", type_str(def)); + kf = g_key_file_new(); + if (ap && ap->backend_settings.nm.name) + g_key_file_set_string(kf, "connection", "id", ap->backend_settings.nm.name); + else if (def->backend_settings.nm.name) + g_key_file_set_string(kf, "connection", "id", def->backend_settings.nm.name); + else { + /* Auto-generate a name for the connection profile, if not specified */ + if (ap) + nd_nm_id = g_strdup_printf("netplan-%s-%s", def->id, ap->ssid); + else + nd_nm_id = g_strdup_printf("netplan-%s", def->id); + g_key_file_set_string(kf, "connection", "id", nd_nm_id); + } + nm_type = type_str(def); + if (nm_type) + g_key_file_set_string(kf, "connection", "type", nm_type); + + if (ap && ap->backend_settings.nm.uuid) + g_key_file_set_string(kf, "connection", "uuid", ap->backend_settings.nm.uuid); + else if (def->backend_settings.nm.uuid) + g_key_file_set_string(kf, "connection", "uuid", def->backend_settings.nm.uuid); /* VLAN devices refer to us as their parent; if our ID is not a name but we * have matches, parent= must be the connection UUID, so put it into the * connection */ if (def->has_vlans && def->has_match) { maybe_generate_uuid(def); uuid_unparse(def->uuid, uuidstr); - g_string_append_printf(s, "uuid=%s\n", uuidstr); + g_key_file_set_string(kf, "connection", "uuid", uuidstr); } if (def->type < NETPLAN_DEF_TYPE_VIRTUAL) { @@ -535,69 +601,70 @@ * supported, MAC matching is done below (different keyfile section), * so only match names here */ if (def->set_name) - g_string_append_printf(s, "interface-name=%s\n", def->set_name); + g_key_file_set_string(kf, "connection", "interface-name", def->set_name); else if (!def->has_match) - g_string_append_printf(s, "interface-name=%s\n", def->id); + g_key_file_set_string(kf, "connection", "interface-name", def->id); else if (def->match.original_name) { if (strpbrk(def->match.original_name, "*[]?")) match_interface_name = def->match.original_name; else - g_string_append_printf(s, "interface-name=%s\n", def->match.original_name); + g_key_file_set_string(kf, "connection", "interface-name", def->match.original_name); } /* else matches on something other than the name, do not restrict interface-name */ } else { /* virtual (created) devices set a name */ - g_string_append_printf(s, "interface-name=%s\n", def->id); + if (strlen(def->id) > 15) + g_debug("interface-name longer than 15 characters is not supported"); + else + g_key_file_set_string(kf, "connection", "interface-name", def->id); if (def->type == NETPLAN_DEF_TYPE_BRIDGE) - write_bridge_params(def, s); + write_bridge_params(def, kf); } if (def->type == NETPLAN_DEF_TYPE_MODEM) { - if (modem_is_gsm(def)) - g_string_append_printf(s, "\n[gsm]\n"); - else - g_string_append_printf(s, "\n[cdma]\n"); + const char* modem_type = modem_is_gsm(def) ? "gsm" : "cdma"; /* Use NetworkManager's auto configuration feature if no APN, username, or password is specified */ if (def->modem_params.auto_config || (!def->modem_params.apn && !def->modem_params.username && !def->modem_params.password)) { - g_string_append_printf(s, "auto-config=true\n"); + g_key_file_set_boolean(kf, modem_type, "auto-config", TRUE); } else { if (def->modem_params.apn) - g_string_append_printf(s, "apn=%s\n", def->modem_params.apn); + g_key_file_set_string(kf, modem_type, "apn", def->modem_params.apn); if (def->modem_params.password) - g_string_append_printf(s, "password=%s\n", def->modem_params.password); + g_key_file_set_string(kf, modem_type, "password", def->modem_params.password); if (def->modem_params.username) - g_string_append_printf(s, "username=%s\n", def->modem_params.username); + g_key_file_set_string(kf, modem_type, "username", def->modem_params.username); } if (def->modem_params.device_id) - g_string_append_printf(s, "device-id=%s\n", def->modem_params.device_id); + g_key_file_set_string(kf, modem_type, "device-id", def->modem_params.device_id); if (def->mtubytes) - g_string_append_printf(s, "mtu=%u\n", def->mtubytes); + g_key_file_set_uint64(kf, modem_type, "mtu", def->mtubytes); if (def->modem_params.network_id) - g_string_append_printf(s, "network-id=%s\n", def->modem_params.network_id); + g_key_file_set_string(kf, modem_type, "network-id", def->modem_params.network_id); if (def->modem_params.number) - g_string_append_printf(s, "number=%s\n", def->modem_params.number); + g_key_file_set_string(kf, modem_type, "number", def->modem_params.number); if (def->modem_params.pin) - g_string_append_printf(s, "pin=%s\n", def->modem_params.pin); + g_key_file_set_string(kf, modem_type, "pin", def->modem_params.pin); if (def->modem_params.sim_id) - g_string_append_printf(s, "sim-id=%s\n", def->modem_params.sim_id); + g_key_file_set_string(kf, modem_type, "sim-id", def->modem_params.sim_id); if (def->modem_params.sim_operator_id) - g_string_append_printf(s, "sim-operator-id=%s\n", def->modem_params.sim_operator_id); + g_key_file_set_string(kf, modem_type, "sim-operator-id", def->modem_params.sim_operator_id); } if (def->bridge) { - g_string_append_printf(s, "slave-type=bridge\nmaster=%s\n", def->bridge); + g_key_file_set_string(kf, "connection", "slave-type", "bridge"); + g_key_file_set_string(kf, "connection", "master", def->bridge); - if (def->bridge_params.path_cost || def->bridge_params.port_priority) - g_string_append_printf(s, "\n[bridge-port]\n"); if (def->bridge_params.path_cost) - g_string_append_printf(s, "path-cost=%u\n", def->bridge_params.path_cost); + g_key_file_set_uint64(kf, "bridge-port", "path-cost", def->bridge_params.path_cost); if (def->bridge_params.port_priority) - g_string_append_printf(s, "priority=%u\n", def->bridge_params.port_priority); + g_key_file_set_uint64(kf, "bridge-port", "priority", def->bridge_params.port_priority); + } + if (def->bond) { + g_key_file_set_string(kf, "connection", "slave-type", "bond"); + g_key_file_set_string(kf, "connection", "master", def->bond); } - if (def->bond) - g_string_append_printf(s, "slave-type=bond\nmaster=%s\n", def->bond); if (def->ipv6_mtubytes) { g_fprintf(stderr, "ERROR: %s: NetworkManager definitions do not support ipv6-mtu\n", def->id); @@ -605,184 +672,176 @@ } if (def->type < NETPLAN_DEF_TYPE_VIRTUAL) { - GString *link_str = NULL; + if (def->type == NETPLAN_DEF_TYPE_ETHERNET) + g_key_file_set_integer(kf, "ethernet", "wake-on-lan", def->wake_on_lan ? 1 : 0); - link_str = g_string_new(NULL); - - g_string_append_printf(s, "\n[ethernet]\nwake-on-lan=%i\n", def->wake_on_lan ? 1 : 0); - - if (!def->set_name && def->match.mac) { - g_string_append_printf(link_str, "mac-address=%s\n", def->match.mac); - } - if (def->set_mac) { - g_string_append_printf(link_str, "cloned-mac-address=%s\n", def->set_mac); - } - if (def->mtubytes) { - g_string_append_printf(link_str, "mtu=%u\n", def->mtubytes); + const char* con_type = NULL; + switch (def->type) { + case NETPLAN_DEF_TYPE_WIFI: + con_type = "wifi"; + case NETPLAN_DEF_TYPE_MODEM: + /* Avoid adding an [ethernet] section into the [gsm/cdma] description. */ + break; + default: + con_type = "ethernet"; } - if (def->wowlan && def->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT) - g_string_append_printf(link_str, "wake-on-wlan=%u\n", def->wowlan); - if (link_str->len > 0) { - switch (def->type) { - case NETPLAN_DEF_TYPE_WIFI: - g_string_append_printf(s, "\n[802-11-wireless]\n%s", link_str->str); break; - case NETPLAN_DEF_TYPE_MODEM: - /* Avoid adding an [ethernet] section into the [gsm/cdma] description. */ - break; - default: - g_string_append_printf(s, "\n[802-3-ethernet]\n%s", link_str->str); break; - } + if (con_type) { + if (!def->set_name && def->match.mac) + g_key_file_set_string(kf, con_type, "mac-address", def->match.mac); + if (def->set_mac) + g_key_file_set_string(kf, con_type, "cloned-mac-address", def->set_mac); + if (def->mtubytes) + g_key_file_set_uint64(kf, con_type, "mtu", def->mtubytes); + if (def->wowlan && def->wowlan > NETPLAN_WIFI_WOWLAN_DEFAULT) + g_key_file_set_uint64(kf, con_type, "wake-on-wlan", def->wowlan); } - - g_string_free(link_str, TRUE); } else { - GString *link_str = NULL; - - link_str = g_string_new(NULL); - - if (def->set_mac) { - g_string_append_printf(link_str, "cloned-mac-address=%s\n", def->set_mac); - } - if (def->mtubytes) { - g_string_append_printf(link_str, "mtu=%u\n", def->mtubytes); - } - - if (link_str->len > 0) { - g_string_append_printf(s, "\n[802-3-ethernet]\n%s", link_str->str); - } - - g_string_free(link_str, TRUE); + if (def->set_mac) + g_key_file_set_string(kf, "ethernet", "cloned-mac-address", def->set_mac); + if (def->mtubytes) + g_key_file_set_uint64(kf, "ethernet", "mtu", def->mtubytes); } if (def->type == NETPLAN_DEF_TYPE_VLAN) { g_assert(def->vlan_id < G_MAXUINT); g_assert(def->vlan_link != NULL); - g_string_append_printf(s, "\n[vlan]\nid=%u\nparent=", def->vlan_id); + g_key_file_set_uint64(kf, "vlan", "id", def->vlan_id); if (def->vlan_link->has_match) { /* we need to refer to the parent's UUID as we don't have an * interface name with match: */ maybe_generate_uuid(def->vlan_link); uuid_unparse(def->vlan_link->uuid, uuidstr); - g_string_append_printf(s, "%s\n", uuidstr); + g_key_file_set_string(kf, "vlan", "parent", uuidstr); } else { /* if we have an interface name, use that as parent */ - g_string_append_printf(s, "%s\n", def->vlan_link->id); + g_key_file_set_string(kf, "vlan", "parent", def->vlan_link->id); } } if (def->type == NETPLAN_DEF_TYPE_BOND) - write_bond_parameters(def, s); + write_bond_parameters(def, kf); if (def->type == NETPLAN_DEF_TYPE_TUNNEL) { if (def->tunnel.mode == NETPLAN_TUNNEL_MODE_WIREGUARD) - write_wireguard_params(def, s); + write_wireguard_params(def, kf); else - write_tunnel_params(def, s); + write_tunnel_params(def, kf); } if (match_interface_name) { - g_string_append(s, "\n[match]\n"); - g_string_append_printf(s, "interface-name=%s;\n", match_interface_name); + const gchar* list[1] = {match_interface_name}; + g_key_file_set_string_list(kf, "match", "interface-name", list, 1); } - g_string_append(s, "\n[ipv4]\n"); - if (ap && ap->mode == NETPLAN_WIFI_MODE_AP) - g_string_append(s, "method=shared\n"); + g_key_file_set_string(kf, "ipv4", "method", "shared"); else if (def->dhcp4) - g_string_append(s, "method=auto\n"); + g_key_file_set_string(kf, "ipv4", "method", "auto"); else if (def->ip4_addresses) /* This requires adding at least one address (done below) */ - g_string_append(s, "method=manual\n"); + g_key_file_set_string(kf, "ipv4", "method", "manual"); else if (def->type == NETPLAN_DEF_TYPE_TUNNEL) /* sit tunnels will not start in link-local apparently */ - g_string_append(s, "method=disabled\n"); + g_key_file_set_string(kf, "ipv4", "method", "disabled"); else /* Without any address, this is the only available mode */ - g_string_append(s, "method=link-local\n"); + g_key_file_set_string(kf, "ipv4", "method", "link-local"); - if (def->ip4_addresses) - for (unsigned i = 0; i < def->ip4_addresses->len; ++i) - g_string_append_printf(s, "address%i=%s\n", i+1, g_array_index(def->ip4_addresses, char*, i)); + if (def->ip4_addresses) { + for (unsigned i = 0; i < def->ip4_addresses->len; ++i) { + tmp_key = g_strdup_printf("address%i", i+1); + g_key_file_set_string(kf, "ipv4", tmp_key, g_array_index(def->ip4_addresses, char*, i)); + g_free(tmp_key); + } + } if (def->gateway4) - g_string_append_printf(s, "gateway=%s\n", def->gateway4); + g_key_file_set_string(kf, "ipv4", "gateway", def->gateway4); if (def->ip4_nameservers) { - g_string_append(s, "dns="); + const gchar* list[def->ip4_nameservers->len]; for (unsigned i = 0; i < def->ip4_nameservers->len; ++i) - g_string_append_printf(s, "%s;", g_array_index(def->ip4_nameservers, char*, i)); - g_string_append(s, "\n"); + list[i] = g_array_index(def->ip4_nameservers, char*, i); + g_key_file_set_string_list(kf, "ipv4", "dns", list, def->ip4_nameservers->len); } /* We can only write search domains and routes if we have an address */ if (def->ip4_addresses || def->dhcp4) { - write_search_domains(def, s); - write_routes(def, s, AF_INET); + write_search_domains(def, "ipv4", kf); + write_routes(def, kf, AF_INET); } if (!def->dhcp4_overrides.use_routes) { - g_string_append(s, "ignore-auto-routes=true\n"); - g_string_append(s, "never-default=true\n"); + g_key_file_set_boolean(kf, "ipv4", "ignore-auto-routes", TRUE); + g_key_file_set_boolean(kf, "ipv4", "never-default", TRUE); } if (def->dhcp4 && def->dhcp4_overrides.metric != NETPLAN_METRIC_UNSPEC) - g_string_append_printf(s, "route-metric=%u\n", def->dhcp4_overrides.metric); + g_key_file_set_uint64(kf, "ipv4", "route-metric", def->dhcp4_overrides.metric); if (def->dhcp6 || def->ip6_addresses || def->gateway6 || def->ip6_nameservers || def->ip6_addr_gen_mode) { - g_string_append(s, "\n[ipv6]\n"); - g_string_append(s, def->dhcp6 ? "method=auto\n" : "method=manual\n"); - if (def->ip6_addresses) - for (unsigned i = 0; i < def->ip6_addresses->len; ++i) - g_string_append_printf(s, "address%i=%s\n", i+1, g_array_index(def->ip6_addresses, char*, i)); + g_key_file_set_string(kf, "ipv6", "method", def->dhcp6 ? "auto" : "manual"); + + if (def->ip6_addresses) { + for (unsigned i = 0; i < def->ip6_addresses->len; ++i) { + tmp_key = g_strdup_printf("address%i", i+1); + g_key_file_set_string(kf, "ipv6", tmp_key, g_array_index(def->ip6_addresses, char*, i)); + g_free(tmp_key); + } + } if (def->ip6_addr_gen_token) { /* Token implies EUI-64, i.e mode=0 */ - g_string_append(s, "addr-gen-mode=0\n"); - g_string_append_printf(s, "token=%s\n", def->ip6_addr_gen_token); - } else if (def->ip6_addr_gen_mode) { - g_string_append_printf(s, "addr-gen-mode=%s\n", addr_gen_mode_str(def->ip6_addr_gen_mode)); - } + g_key_file_set_integer(kf, "ipv6", "addr-gen-mode", 0); + g_key_file_set_string(kf, "ipv6", "token", def->ip6_addr_gen_token); + } else if (def->ip6_addr_gen_mode) + g_key_file_set_string(kf, "ipv6", "addr-gen-mode", addr_gen_mode_str(def->ip6_addr_gen_mode)); if (def->ip6_privacy) - g_string_append(s, "ip6-privacy=2\n"); + g_key_file_set_integer(kf, "ipv6", "ip6-privacy", 2); if (def->gateway6) - g_string_append_printf(s, "gateway=%s\n", def->gateway6); + g_key_file_set_string(kf, "ipv6", "gateway", def->gateway6); if (def->ip6_nameservers) { - g_string_append(s, "dns="); + const gchar* list[def->ip6_nameservers->len]; for (unsigned i = 0; i < def->ip6_nameservers->len; ++i) - g_string_append_printf(s, "%s;", g_array_index(def->ip6_nameservers, char*, i)); - g_string_append(s, "\n"); + list[i] = g_array_index(def->ip6_nameservers, char*, i); + g_key_file_set_string_list(kf, "ipv6", "dns", list, def->ip6_nameservers->len); } /* nm-settings(5) specifies search-domain for both [ipv4] and [ipv6] -- * We need to specify it here for the IPv6-only case - see LP: #1786726 */ - write_search_domains(def, s); + write_search_domains(def, "ipv6", kf); /* We can only write valid routes if there is a DHCPv6 or static IPv6 address */ - write_routes(def, s, AF_INET6); + write_routes(def, kf, AF_INET6); if (!def->dhcp6_overrides.use_routes) { - g_string_append(s, "ignore-auto-routes=true\n"); - g_string_append(s, "never-default=true\n"); + g_key_file_set_boolean(kf, "ipv6", "ignore-auto-routes", TRUE); + g_key_file_set_boolean(kf, "ipv6", "never-default", TRUE); } if (def->dhcp6_overrides.metric != NETPLAN_METRIC_UNSPEC) - g_string_append_printf(s, "route-metric=%u\n", def->dhcp6_overrides.metric); + g_key_file_set_uint64(kf, "ipv6", "route-metric", def->dhcp6_overrides.metric); } - else { - g_string_append(s, "\n[ipv6]\nmethod=ignore\n"); + else + g_key_file_set_string(kf, "ipv6", "method", "ignore"); + + if (def->backend_settings.nm.passthrough) { + g_debug("NetworkManager: using keyfile passthrough mode"); + /* Write all key-value pairs from the hashtable into the keyfile, + * potentially overriding existing values, if not fully supported. */ + g_datalist_foreach(&def->backend_settings.nm.passthrough, write_fallback_key_value, kf); } if (ap) { g_autofree char* escaped_ssid = g_uri_escape_string(ap->ssid, NULL, TRUE); conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, "-", escaped_ssid, ".nmconnection", NULL); - g_string_append_printf(s, "\n[wifi]\nssid=%s\nmode=%s\n", ap->ssid, wifi_mode_str(ap->mode)); - if (ap->bssid) { - g_string_append_printf(s, "bssid=%s\n", ap->bssid); - } - if (ap->hidden) { - g_string_append(s, "hidden=true\n"); - } + g_key_file_set_string(kf, "wifi", "ssid", ap->ssid); + if (ap->mode < NETPLAN_WIFI_MODE_OTHER) + g_key_file_set_string(kf, "wifi", "mode", wifi_mode_str(ap->mode)); + if (ap->bssid) + g_key_file_set_string(kf, "wifi", "bssid", ap->bssid); + if (ap->hidden) + g_key_file_set_boolean(kf, "wifi", "hidden", TRUE); if (ap->band == NETPLAN_WIFI_BAND_5 || ap->band == NETPLAN_WIFI_BAND_24) { - g_string_append_printf(s, "band=%s\n", wifi_band_str(ap->band)); + g_key_file_set_string(kf, "wifi", "band", wifi_band_str(ap->band)); /* Channel is only unambiguous, if band is set. */ if (ap->channel) { /* Validate WiFi channel */ @@ -790,22 +849,38 @@ wifi_get_freq5(ap->channel); else wifi_get_freq24(ap->channel); - g_string_append_printf(s, "channel=%u\n", ap->channel); + g_key_file_set_uint64(kf, "wifi", "channel", ap->channel); } } if (ap->has_auth) { - write_wifi_auth_parameters(&ap->auth, s); + write_wifi_auth_parameters(&ap->auth, kf); + } + if (ap->backend_settings.nm.passthrough) { + g_debug("NetworkManager: using AP keyfile passthrough mode"); + /* Write all key-value pairs from the hashtable into the keyfile, + * potentially overriding existing values, if not fully supported. + * AP passthrough values have higher priority than ND passthrough, + * because they are more specific and bound to the current SSID's + * NM connection profile. */ + g_datalist_foreach((GData**)&ap->backend_settings.nm.passthrough, write_fallback_key_value, kf); } } else { conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, ".nmconnection", NULL); if (def->has_auth) { - write_dot1x_auth_parameters(&def->auth, s); + write_dot1x_auth_parameters(&def->auth, kf); } } /* NM connection files might contain secrets, and NM insists on tight permissions */ + full_path = g_strjoin(G_DIR_SEPARATOR_S, rootdir ?: "", conf_path, NULL); orig_umask = umask(077); - g_string_free_to_file(s, rootdir, conf_path, NULL); + safe_mkdir_p_dir(full_path); + if (!g_key_file_save_to_file(kf, full_path, &error)) { + // LCOV_EXCL_START + g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", full_path, error->message); + exit(1); + // LCOV_EXCL_STO + } umask(orig_umask); } @@ -833,7 +908,6 @@ exit(1); } - /* for wifi we need to create a separate connection file for every SSID */ if (def->type == NETPLAN_DEF_TYPE_WIFI) { GHashTableIter iter; gpointer key; @@ -872,7 +946,7 @@ GString *s = NULL; gsize len; - if (g_hash_table_size(netdefs) == 0) + if (!netdefs || g_hash_table_size(netdefs) == 0) return; /* Set all devices not managed by us to unmanaged, so that NM does not diff -Nru netplan.io-0.101/src/parse.c netplan.io-0.102/src/parse.c --- netplan.io-0.101/src/parse.c 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/src/parse.c 2021-03-16 11:38:25.000000000 +0000 @@ -28,6 +28,7 @@ #include #include "parse.h" +#include "util.h" #include "error.h" #include "validation.h" @@ -58,6 +59,9 @@ static NetplanIPRoute* cur_route; static NetplanIPRule* cur_ip_rule; +/* Filename of the currently parsed YAML file */ +const char* cur_filename; + static NetplanBackend backend_global, backend_cur_type; /* global OpenVSwitch settings */ @@ -211,7 +215,7 @@ ovs_settings->rstp = FALSE; } -static NetplanNetDefinition* +NetplanNetDefinition* netplan_netdef_new(const char* id, NetplanDefType type, NetplanBackend backend) { /* create new network definition */ @@ -236,6 +240,8 @@ /* OpenVSwitch defaults */ initialize_ovs_settings(&cur_netdef->ovs_settings); + if (!netdefs) + netdefs = g_hash_table_new(g_str_hash, g_str_equal); g_hash_table_insert(netdefs, cur_netdef->id, cur_netdef); netdefs_ordered = g_list_append(netdefs_ordered, cur_netdef); return cur_netdef; @@ -446,6 +452,34 @@ return TRUE; } +/* + * Handler for setting a DataList field from a mapping node, inside a given struct + * @entryptr: pointer to the beginning of the to-be-modified data structure + * @data: offset into entryptr struct where the boolean field to write is located +*/ +static gboolean +handle_generic_datalist(yaml_document_t* doc, yaml_node_t* node, void* entryptr, const void* data, GError** error) +{ + guint offset = GPOINTER_TO_UINT(data); + GData** list = (GData**) ((void*) entryptr + offset); + if (!*list) + g_datalist_init(list); + + for (yaml_node_pair_t* entry = node->data.mapping.pairs.start; entry < node->data.mapping.pairs.top; entry++) { + yaml_node_t* key, *value; + + key = yaml_document_get_node(doc, entry->key); + value = yaml_document_get_node(doc, entry->value); + + assert_type(key, YAML_SCALAR_NODE); + assert_type(value, YAML_SCALAR_NODE); + + g_datalist_set_data_full(list, g_strdup(scalar(key)), g_strdup(scalar(value)), g_free); + } + + return TRUE; +} + /** * Generic handler for setting a cur_netdef string field from a scalar node * @data: offset into NetplanNetDefinition where the const char* field to write is @@ -620,6 +654,13 @@ return handle_generic_map(doc, node, cur_netdef, data, error); } +static gboolean +handle_netdef_datalist(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_netdef); + return handle_generic_datalist(doc, node, cur_netdef, data, error); +} + /**************************************************** * Grammar and handlers for network config "match" entry ****************************************************/ @@ -708,6 +749,19 @@ } static gboolean +handle_access_point_str(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + return handle_generic_str(doc, node, cur_access_point, data, error); +} + +static gboolean +handle_access_point_datalist(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) +{ + g_assert(cur_access_point); + return handle_generic_datalist(doc, node, cur_access_point, data, error); +} + +static gboolean handle_access_point_guint(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) { return handle_generic_guint(doc, node, cur_access_point, data, error); @@ -780,6 +834,29 @@ return TRUE; } +/* Keep in sync with ap_nm_backend_settings_handlers */ +static const mapping_entry_handler nm_backend_settings_handlers[] = { + {"name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.name)}, + {"uuid", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.uuid)}, + {"stable-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.stable_id)}, + {"device", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.device)}, + /* Fallback mode, to support all NM settings of the NetworkManager netplan backend */ + {"passthrough", YAML_MAPPING_NODE, handle_netdef_datalist, NULL, netdef_offset(backend_settings.nm.passthrough)}, + {NULL} +}; + +/* Keep in sync with nm_backend_settings_handlers */ +static const mapping_entry_handler ap_nm_backend_settings_handlers[] = { + {"name", YAML_SCALAR_NODE, handle_access_point_str, NULL, access_point_offset(backend_settings.nm.name)}, + {"uuid", YAML_SCALAR_NODE, handle_access_point_str, NULL, access_point_offset(backend_settings.nm.uuid)}, + {"stable-id", YAML_SCALAR_NODE, handle_access_point_str, NULL, access_point_offset(backend_settings.nm.stable_id)}, + {"device", YAML_SCALAR_NODE, handle_access_point_str, NULL, access_point_offset(backend_settings.nm.device)}, + /* Fallback mode, to support all NM settings of the NetworkManager netplan backend */ + {"passthrough", YAML_MAPPING_NODE, handle_access_point_datalist, NULL, access_point_offset(backend_settings.nm.passthrough)}, + {NULL} +}; + + static const mapping_entry_handler wifi_access_point_handlers[] = { {"band", YAML_SCALAR_NODE, handle_access_point_band}, {"bssid", YAML_SCALAR_NODE, handle_access_point_mac, NULL, access_point_offset(bssid)}, @@ -788,6 +865,7 @@ {"mode", YAML_SCALAR_NODE, handle_access_point_mode}, {"password", YAML_SCALAR_NODE, handle_access_point_password}, {"auth", YAML_MAPPING_NODE, handle_access_point_auth}, + {"networkmanager", YAML_MAPPING_NODE, NULL, ap_nm_backend_settings_handlers}, {NULL} }; @@ -1518,6 +1596,8 @@ {"via", YAML_SCALAR_NODE, handle_routes_ip, NULL, route_offset(via)}, {"metric", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(metric)}, {"mtu", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(mtubytes)}, + {"congestion-window", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(congestion_window)}, + {"advertised-receive-window", YAML_SCALAR_NODE, handle_routes_guint, NULL, route_offset(advertised_receive_window)}, {NULL} }; @@ -1622,6 +1702,18 @@ static gboolean handle_arp_ip_targets(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error) { + if (!cur_netdef->bond_params.arp_ip_targets) { + cur_netdef->bond_params.arp_ip_targets = g_array_new(FALSE, FALSE, sizeof(char *)); + } + + /* Avoid adding the same arp_ip_targets in a 2nd parsing pass by comparing + * the array size to the YAML sequence size. Skip if they are equal. */ + guint item_count = node->data.sequence.items.top - node->data.sequence.items.start; + if (cur_netdef->bond_params.arp_ip_targets->len == item_count) { + g_debug("%s: all arp ip targets have already been added", cur_netdef->id); + return TRUE; + } + for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { g_autofree char* addr = NULL; yaml_node_t *entry = yaml_document_get_node(doc, *i); @@ -1631,8 +1723,6 @@ /* is it an IPv4 address? */ if (is_ip4_address(addr)) { - if (!cur_netdef->bond_params.arp_ip_targets) - cur_netdef->bond_params.arp_ip_targets = g_array_new(FALSE, FALSE, sizeof(char*)); char* s = g_strdup(scalar(entry)); g_array_append_val(cur_netdef->bond_params.arp_ip_targets, s); continue; @@ -1899,7 +1989,6 @@ } for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { - g_autofree char* addr = NULL; yaml_node_t *entry = yaml_document_get_node(doc, *i); assert_type(entry, YAML_MAPPING_NODE); @@ -1922,19 +2011,11 @@ * Grammar and handlers for network devices ****************************************************/ -static const mapping_entry_handler nm_backend_settings_handlers[] = { - {"name", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.name)}, - {"uuid", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.uuid)}, - {"stable-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.stable_id)}, - {"device", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(backend_settings.nm.device)}, - {NULL} -}; - static gboolean handle_ovs_bond_lacp(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) { if (cur_netdef->type != NETPLAN_DEF_TYPE_BOND) - return yaml_error(node, error, "Key 'lacp' is only valid for iterface type 'openvswitch bond'"); + return yaml_error(node, error, "Key 'lacp' is only valid for interface type 'openvswitch bond'"); if (g_strcmp0(scalar(node), "active") && g_strcmp0(scalar(node), "passive") && g_strcmp0(scalar(node), "off")) return yaml_error(node, error, "Value of 'lacp' needs to be 'active', 'passive' or 'off"); @@ -1946,7 +2027,7 @@ handle_ovs_bridge_bool(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) { if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) - return yaml_error(node, error, "Key is only valid for iterface type 'openvswitch bridge'"); + return yaml_error(node, error, "Key is only valid for interface type 'openvswitch bridge'"); return handle_netdef_bool(doc, node, data, error); } @@ -1955,7 +2036,7 @@ handle_ovs_bridge_fail_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) { if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) - return yaml_error(node, error, "Key 'fail-mode' is only valid for iterface type 'openvswitch bridge'"); + return yaml_error(node, error, "Key 'fail-mode' is only valid for interface type 'openvswitch bridge'"); if (g_strcmp0(scalar(node), "standalone") && g_strcmp0(scalar(node), "secure")) return yaml_error(node, error, "Value of 'fail-mode' needs to be 'standalone' or 'secure'"); @@ -1997,7 +2078,7 @@ handle_ovs_bridge_protocol(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) { if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) - return yaml_error(node, error, "Key 'protocols' is only valid for iterface type 'openvswitch bridge'"); + return yaml_error(node, error, "Key 'protocols' is only valid for interface type 'openvswitch bridge'"); return handle_ovs_protocol(doc, node, cur_netdef, data, error); } @@ -2006,7 +2087,7 @@ handle_ovs_bridge_controller_connection_mode(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) { if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) - return yaml_error(node, error, "Key 'controller.connection-mode' is only valid for iterface type 'openvswitch bridge'"); + return yaml_error(node, error, "Key 'controller.connection-mode' is only valid for interface type 'openvswitch bridge'"); if (g_strcmp0(scalar(node), "in-band") && g_strcmp0(scalar(node), "out-of-band")) return yaml_error(node, error, "Value of 'connection-mode' needs to be 'in-band' or 'out-of-band'"); @@ -2018,7 +2099,7 @@ handle_ovs_bridge_controller_addresses(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error) { if (cur_netdef->type != NETPLAN_DEF_TYPE_BRIDGE) - return yaml_error(node, error, "Key 'controller.addresses' is only valid for iterface type 'openvswitch bridge'"); + return yaml_error(node, error, "Key 'controller.addresses' is only valid for interface type 'openvswitch bridge'"); for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) { gchar** vec = NULL; @@ -2222,6 +2303,8 @@ static const mapping_entry_handler modem_def_handlers[] = { COMMON_LINK_HANDLERS, + COMMON_BACKEND_HANDLERS, + PHYSICAL_LINK_HANDLERS, {"apn", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.apn)}, {"auto-config", YAML_SCALAR_NODE, handle_netdef_bool, NULL, netdef_offset(modem_params.auto_config)}, {"device-id", YAML_SCALAR_NODE, handle_netdef_str, NULL, netdef_offset(modem_params.device_id)}, @@ -2240,6 +2323,7 @@ {"mode", YAML_SCALAR_NODE, handle_tunnel_mode}, {"local", YAML_SCALAR_NODE, handle_tunnel_addr, NULL, netdef_offset(tunnel.local_ip)}, {"remote", YAML_SCALAR_NODE, handle_tunnel_addr, NULL, netdef_offset(tunnel.remote_ip)}, + {"ttl", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(tunnel.ttl)}, /* Handle key/keys for clarity in config: this can be either a scalar or * mapping of multiple keys (input and output) @@ -2313,7 +2397,7 @@ assert_type(peer, YAML_SCALAR_NODE); /* Create port 1 netdef */ - component = g_hash_table_lookup(netdefs, scalar(port)); + component = netdefs ? g_hash_table_lookup(netdefs, scalar(port)) : NULL; if (!component) { component = netplan_netdef_new(scalar(port), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS); if (g_hash_table_remove(missing_id, scalar(port))) @@ -2327,7 +2411,7 @@ /* Create port 2 (peer) netdef */ component = NULL; - component = g_hash_table_lookup(netdefs, scalar(peer)); + component = netdefs ? g_hash_table_lookup(netdefs, scalar(peer)) : NULL; if (!component) { component = netplan_netdef_new(scalar(peer), NETPLAN_DEF_TYPE_PORT, NETPLAN_BACKEND_OVS); if (g_hash_table_remove(missing_id, scalar(peer))) @@ -2377,14 +2461,16 @@ if(g_hash_table_remove(missing_id, scalar(key))) missing_ids_found++; - cur_netdef = g_hash_table_lookup(netdefs, scalar(key)); + cur_netdef = netdefs ? g_hash_table_lookup(netdefs, scalar(key)) : NULL; if (cur_netdef) { /* already exists, overriding/amending previous definition */ if (cur_netdef->type != GPOINTER_TO_UINT(data)) return yaml_error(key, error, "Updated definition '%s' changes device type", scalar(key)); } else { - netplan_netdef_new(scalar(key), GPOINTER_TO_UINT(data), backend_cur_type); + cur_netdef = netplan_netdef_new(scalar(key), GPOINTER_TO_UINT(data), backend_cur_type); } + g_assert(cur_filename); + cur_netdef->filename = g_strdup(cur_filename); // XXX: breaks multi-pass parsing. //if (!g_hash_table_add(ids_in_file, cur_netdef->id)) @@ -2399,6 +2485,10 @@ case NETPLAN_DEF_TYPE_TUNNEL: handlers = tunnel_def_handlers; break; case NETPLAN_DEF_TYPE_VLAN: handlers = vlan_def_handlers; break; case NETPLAN_DEF_TYPE_WIFI: handlers = wifi_def_handlers; break; + case NETPLAN_DEF_TYPE_NM: + g_warning("netplan: %s: handling NetworkManager passthrough device, settings are not fully supported.", cur_netdef->id); + handlers = ethernet_def_handlers; + break; default: g_assert_not_reached(); // LCOV_EXCL_LINE } if (!process_mapping(doc, value, handlers, NULL, error)) @@ -2411,7 +2501,7 @@ /* convenience shortcut: physical device without match: means match * name on ID */ if (cur_netdef->type < NETPLAN_DEF_TYPE_VIRTUAL && !cur_netdef->has_match) - cur_netdef->match.original_name = cur_netdef->id; + cur_netdef->match.original_name = g_strdup(cur_netdef->id); } backend_cur_type = NETPLAN_BACKEND_NONE; return TRUE; @@ -2455,6 +2545,7 @@ {"vlans", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_VLAN)}, {"wifis", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_WIFI)}, {"modems", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_MODEM)}, + {"nm-devices", YAML_MAPPING_NODE, handle_network_type, NULL, GUINT_TO_POINTER(NETPLAN_DEF_TYPE_NM)}, {"openvswitch", YAML_MAPPING_NODE, NULL, ovs_network_settings_handlers}, {NULL} }; @@ -2532,9 +2623,6 @@ if (!load_yaml(filename, &doc, error)) return FALSE; - if (!netdefs) - netdefs = g_hash_table_new(g_str_hash, g_str_equal); - /* empty file? */ if (yaml_document_get_root_node(&doc) == NULL) return TRUE; @@ -2542,8 +2630,10 @@ g_assert(ids_in_file == NULL); ids_in_file = g_hash_table_new(g_str_hash, NULL); + cur_filename = filename; ret = process_document(&doc, error); + cur_filename = NULL; cur_netdef = NULL; yaml_document_delete(&doc); g_hash_table_destroy(ids_in_file); @@ -2606,6 +2696,49 @@ /* FIXME: make sure that any dynamically allocated netdef data is freed */ if (n > 0) g_hash_table_remove_all(netdefs); + netdefs = NULL; } + if(netdefs_ordered) { + g_clear_list(&netdefs_ordered, g_free); + netdefs_ordered = NULL; + } return n; } + +void +process_input_file(const char* f) +{ + GError* error = NULL; + + g_debug("Processing input file %s..", f); + if (!netplan_parse_yaml(f, &error)) { + g_fprintf(stderr, "%s\n", error->message); + exit(1); + } +} + +gboolean +process_yaml_hierarchy(const char* rootdir) +{ + glob_t gl; + /* Files with asciibetically higher names override/append settings from + * earlier ones (in all config dirs); files in /run/netplan/ + * shadow files in /etc/netplan/ which shadow files in /lib/netplan/. + * To do that, we put all found files in a hash table, then sort it by + * file name, and add the entries from /run after the ones from /etc + * and those after the ones from /lib. */ + if (find_yaml_glob(rootdir, &gl) != 0) + return FALSE; // LCOV_EXCL_LINE + /* keys are strdup()ed, free them; values point into the glob_t, don't free them */ + g_autoptr(GHashTable) configs = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); + g_autoptr(GList) config_keys = NULL; + + for (size_t i = 0; i < gl.gl_pathc; ++i) + g_hash_table_insert(configs, g_path_get_basename(gl.gl_pathv[i]), gl.gl_pathv[i]); + + config_keys = g_list_sort(g_hash_table_get_keys(configs), (GCompareFunc) strcmp); + + for (GList* i = config_keys; i != NULL; i = i->next) + process_input_file(g_hash_table_lookup(configs, i->data)); + return TRUE; +} diff -Nru netplan.io-0.101/src/parse.h netplan.io-0.102/src/parse.h --- netplan.io-0.101/src/parse.h 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/src/parse.h 2021-03-16 11:38:25.000000000 +0000 @@ -51,6 +51,9 @@ NETPLAN_DEF_TYPE_VLAN, NETPLAN_DEF_TYPE_TUNNEL, NETPLAN_DEF_TYPE_PORT, + /* Type fallback/passthrough */ + NETPLAN_DEF_TYPE_NM, + NETPLAN_DEF_TYPE_MAX_ } NetplanDefType; typedef enum { @@ -218,6 +221,19 @@ NetplanAuthenticationSettings ssl; } NetplanOVSSettings; +typedef union { + struct NetplanNMSettings { + char *name; + char *uuid; + char *stable_id; + char *device; + GData* passthrough; + } nm; + struct NetplanNetworkdSettings { + char *unit; + } networkd; +} NetplanBackendSettings; + /** * Represent a configuration stanza */ @@ -230,6 +246,7 @@ NetplanDefType type; NetplanBackend backend; char* id; + char* filename; /* only necessary for NetworkManager connection UUIDs in some cases */ uuid_t uuid; @@ -359,6 +376,7 @@ char *private_key; /* used for wireguard */ guint fwmark; guint port; + guint ttl; } tunnel; NetplanAuthenticationSettings auth; @@ -374,25 +392,24 @@ /* netplan-feature: openvswitch */ NetplanOVSSettings ovs_settings; - union { - struct NetplanNMSettings { - char *name; - char *uuid; - char *stable_id; - char *device; - } nm; - struct NetplanNetworkdSettings { - char *unit; - } networkd; - } backend_settings; + NetplanBackendSettings backend_settings; }; typedef enum { NETPLAN_WIFI_MODE_INFRASTRUCTURE, NETPLAN_WIFI_MODE_ADHOC, - NETPLAN_WIFI_MODE_AP + NETPLAN_WIFI_MODE_AP, + NETPLAN_WIFI_MODE_OTHER, + NETPLAN_WIFI_MODE_MAX_ } NetplanWifiMode; +static const char* const netplan_wifi_mode_to_str[NETPLAN_WIFI_MODE_MAX_] = { + [NETPLAN_WIFI_MODE_INFRASTRUCTURE] = "infrastructure", + [NETPLAN_WIFI_MODE_ADHOC] = "adhoc", + [NETPLAN_WIFI_MODE_AP] = "ap", + [NETPLAN_WIFI_MODE_OTHER] = NULL, +}; + typedef struct { char *endpoint; char *public_key; @@ -423,8 +440,12 @@ NetplanAuthenticationSettings auth; gboolean has_auth; + + NetplanBackendSettings backend_settings; } NetplanWifiAccessPoint; +#define NETPLAN_ADVERTISED_RECEIVE_WINDOW_UNSPEC 0 +#define NETPLAN_CONGESTION_WINDOW_UNSPEC 0 #define NETPLAN_MTU_UNSPEC 0 #define NETPLAN_METRIC_UNSPEC G_MAXUINT #define NETPLAN_ROUTE_TABLE_UNSPEC 0 @@ -449,6 +470,8 @@ guint metric; guint mtubytes; + guint congestion_window; + guint advertised_receive_window; } NetplanIPRoute; typedef struct { @@ -477,5 +500,10 @@ gboolean netplan_parse_yaml(const char* filename, GError** error); GHashTable* netplan_finish_parse(GError** error); +guint netplan_clear_netdefs(); NetplanBackend netplan_get_global_backend(); const char* tunnel_mode_to_string(NetplanTunnelMode mode); +NetplanNetDefinition* netplan_netdef_new(const char* id, NetplanDefType type, NetplanBackend renderer); + +void process_input_file(const char* f); +gboolean process_yaml_hierarchy(const char* rootdir); diff -Nru netplan.io-0.101/src/parse-nm.c netplan.io-0.102/src/parse-nm.c --- netplan.io-0.101/src/parse-nm.c 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/src/parse-nm.c 2021-03-16 11:38:25.000000000 +0000 @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include + +#include "netplan.h" +#include "parse-nm.h" +#include "parse.h" +#include "util.h" + +/** + * NetworkManager writes the alias for '802-3-ethernet' (ethernet), + * '802-11-wireless' (wifi) and '802-11-wireless-security' (wifi-security) + * by default, so we only need to check for those. See: + * https://bugzilla.gnome.org/show_bug.cgi?id=696940 + * https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/commit/c36200a225aefb2a3919618e75682646899b82c0 + */ +static const NetplanDefType +type_from_str(const char* type_str) +{ + if (!g_strcmp0(type_str, "ethernet") || !g_strcmp0(type_str, "802-3-ethernet")) + return NETPLAN_DEF_TYPE_ETHERNET; + else if (!g_strcmp0(type_str, "wifi") || !g_strcmp0(type_str, "802-11-wireless")) + return NETPLAN_DEF_TYPE_WIFI; + else if (!g_strcmp0(type_str, "gsm") || !g_strcmp0(type_str, "cdma")) + return NETPLAN_DEF_TYPE_MODEM; + else if (!g_strcmp0(type_str, "bridge")) + return NETPLAN_DEF_TYPE_BRIDGE; + else if (!g_strcmp0(type_str, "bond")) + return NETPLAN_DEF_TYPE_BOND; + else if (!g_strcmp0(type_str, "vlan")) + return NETPLAN_DEF_TYPE_VLAN; + else if (!g_strcmp0(type_str, "ip-tunnel") || !g_strcmp0(type_str, "wireguard")) + return NETPLAN_DEF_TYPE_TUNNEL; + /* Unsupported type, needs to be specified via passthrough */ + return NETPLAN_DEF_TYPE_NM; +} + +static const NetplanWifiMode +ap_type_from_str(const char* type_str) +{ + if (!g_strcmp0(type_str, "infrastructure")) + return NETPLAN_WIFI_MODE_INFRASTRUCTURE; + else if (!g_strcmp0(type_str, "ap")) + return NETPLAN_WIFI_MODE_AP; + else if (!g_strcmp0(type_str, "adhoc")) + return NETPLAN_WIFI_MODE_ADHOC; + /* Unsupported mode, like "mesh" */ + return NETPLAN_WIFI_MODE_OTHER; +} + +static gboolean +_kf_clear_key(GKeyFile* kf, const gchar* group, const gchar* key) +{ + gsize len = 1; + gboolean ret = FALSE; + ret = g_key_file_remove_key(kf, group, key, NULL); + g_strfreev(g_key_file_get_keys(kf, group, &len, NULL)); + /* clear group if this was the last key */ + if (len == 0) + ret &= g_key_file_remove_group(kf, group, NULL); + return ret; +} + +/* Read the key-value pairs from the keyfile and pass them through to a map */ +static void +read_passthrough(GKeyFile* kf, GData** list) +{ + gchar **groups = NULL; + gchar **keys = NULL; + gchar *group_key = NULL; + gchar *value = NULL; + gsize klen = 0; + gsize glen = 0; + + if (!*list) + g_datalist_init(list); + groups = g_key_file_get_groups(kf, &glen); + if (groups) { + for (unsigned i = 0; i < glen; ++i) { + klen = 0; + keys = g_key_file_get_keys(kf, groups[i], &klen, NULL); + if (klen == 0) { + /* empty group */ + g_datalist_set_data_full(list, g_strconcat(groups[i], ".", NETPLAN_NM_EMPTY_GROUP, NULL), g_strdup(""), g_free); + continue; + } + for (unsigned j = 0; j < klen; ++j) { + value = g_key_file_get_string(kf, groups[i], keys[j], NULL); + if (!value) { + // LCOV_EXCL_START + g_warning("netplan: Keyfile: cannot read value of %s.%s", groups[i], keys[j]); + continue; + // LCOV_EXCL_STOP + } + group_key = g_strconcat(groups[i], ".", keys[j], NULL); + g_datalist_set_data_full(list, group_key, value, g_free); + /* no need to free group_key and value: they stay in the list */ + } + g_strfreev(keys); + } + g_strfreev(groups); + } +} + +/** + * Parse keyfile into a NetplanNetDefinition struct + * @filename: full path to the NetworkManager keyfile + */ +gboolean +netplan_parse_keyfile(const char* filename, GError** error) +{ + g_autofree gchar *nd_id = NULL; + g_autofree gchar *uuid = NULL; + g_autofree gchar *type = NULL; + g_autofree gchar* wifi_mode = NULL; + g_autofree gchar* ssid = NULL; + g_autofree gchar* netdef_id = NULL; + NetplanNetDefinition* nd = NULL; + NetplanWifiAccessPoint* ap = NULL; + g_autoptr(GKeyFile) kf = g_key_file_new(); + NetplanDefType nd_type = NETPLAN_DEF_TYPE_NONE; + if (!g_key_file_load_from_file(kf, filename, G_KEY_FILE_NONE, error)) { + g_warning("netplan: cannot load keyfile"); + return FALSE; + } + + ssid = g_key_file_get_string(kf, "wifi", "ssid", NULL); + if (!ssid) + ssid = g_key_file_get_string(kf, "802-11-wireless", "ssid", NULL); + + netdef_id = netplan_get_id_from_nm_filename(filename, ssid); + uuid = g_key_file_get_string(kf, "connection", "uuid", NULL); + if (!uuid) { + g_warning("netplan: Keyfile: cannot find connection.uuid"); + return FALSE; + } + + type = g_key_file_get_string(kf, "connection", "type", NULL); + if (!type) { + g_warning("netplan: Keyfile: cannot find connection.type"); + return FALSE; + } + nd_type = type_from_str(type); + + /* Use previously existing netdef IDs, if available, to override connections + * Else: generate a "NM-" ID */ + if (netdef_id) + nd_id = g_strdup(netdef_id); + else + nd_id = g_strconcat("NM-", uuid, NULL); + nd = netplan_netdef_new(nd_id, nd_type, NETPLAN_BACKEND_NM); + + /* Handle uuid & NM name/id */ + nd->backend_settings.nm.uuid = g_strdup(uuid); + _kf_clear_key(kf, "connection", "uuid"); + nd->backend_settings.nm.name = g_key_file_get_string(kf, "connection", "id", NULL); + if (nd->backend_settings.nm.name) + _kf_clear_key(kf, "connection", "id"); + + if (nd_type == NETPLAN_DEF_TYPE_NM) + goto only_passthrough; //do not try to handle any keys for connections types unknown to netplan + + /* remove supported values from passthrough, which have been handled */ + if ( nd_type == NETPLAN_DEF_TYPE_ETHERNET + || nd_type == NETPLAN_DEF_TYPE_WIFI + || nd_type == NETPLAN_DEF_TYPE_MODEM + || nd_type == NETPLAN_DEF_TYPE_BRIDGE + || nd_type == NETPLAN_DEF_TYPE_BOND + || nd_type == NETPLAN_DEF_TYPE_VLAN) + _kf_clear_key(kf, "connection", "type"); + + /* Handle match: Netplan usually defines a connection per interface, while + * NM connection profiles are usually applied to any interface of matching + * type (like wifi/ethernet/...). */ + if (nd->type < NETPLAN_DEF_TYPE_VIRTUAL) { + nd->match.original_name = g_key_file_get_string(kf, "connection", "interface-name", NULL); + if (nd->match.original_name) + _kf_clear_key(kf, "connection", "interface-name"); + /* Set match, even if it is empty, so the NM renderer will not force + * the netdef ID as interface-name */ + nd->has_match = TRUE; + } + + /* Modem parameters + * NM differentiates between GSM and CDMA connections, while netplan + * combines them as "modems". We need to parse a basic set of parameters + * to enable the generator (in nm.c) to detect GSM vs CDMA connections, + * using its modem_is_gsm() util. */ + nd->modem_params.auto_config = g_key_file_get_boolean(kf, "gsm", "auto-config", NULL); + _kf_clear_key(kf, "gsm", "auto-config"); + nd->modem_params.apn = g_key_file_get_string(kf, "gsm", "apn", NULL); + if (nd->modem_params.apn) + _kf_clear_key(kf, "gsm", "apn"); + nd->modem_params.device_id = g_key_file_get_string(kf, "gsm", "device-id", NULL); + if (nd->modem_params.device_id) + _kf_clear_key(kf, "gsm", "device-id"); + nd->modem_params.network_id = g_key_file_get_string(kf, "gsm", "network-id", NULL); + if (nd->modem_params.network_id) + _kf_clear_key(kf, "gsm", "network-id"); + nd->modem_params.pin = g_key_file_get_string(kf, "gsm", "pin", NULL); + if (nd->modem_params.pin) + _kf_clear_key(kf, "gsm", "pin"); + nd->modem_params.sim_id = g_key_file_get_string(kf, "gsm", "sim-id", NULL); + if (nd->modem_params.sim_id) + _kf_clear_key(kf, "gsm", "sim-id"); + nd->modem_params.sim_operator_id = g_key_file_get_string(kf, "gsm", "sim-operator-id", NULL); + if (nd->modem_params.sim_operator_id) + _kf_clear_key(kf, "gsm", "sim-operator-id"); + + /* wake-on-lan, do not clear passthrough as we do not fully support this setting */ + if (g_key_file_has_group(kf, "ethernet")) { + if (!g_key_file_has_key(kf, "ethernet", "wake-on-lan", NULL)) { + nd->wake_on_lan = TRUE; //NM's default is "1" + } else { + //XXX: fix delta between options in NM (0x1, 0x2, 0x4, ...) and netplan (bool) + nd->wake_on_lan = g_key_file_get_uint64(kf, "ethernet", "wake-on-lan", NULL) > 0; + } + } + + /* Special handling for WiFi "access-points:" mapping */ + if (nd->type == NETPLAN_DEF_TYPE_WIFI) { + ap = g_new0(NetplanWifiAccessPoint, 1); + ap->ssid = g_key_file_get_string(kf, "wifi", "ssid", NULL); + if (!ap->ssid) { + g_warning("netplan: Keyfile: cannot find SSID for WiFi connection"); + return FALSE; + } else + _kf_clear_key(kf, "wifi", "ssid"); + + wifi_mode = g_key_file_get_string(kf, "wifi", "mode", NULL); + if (wifi_mode) { + ap->mode = ap_type_from_str(wifi_mode); + if (ap->mode != NETPLAN_WIFI_MODE_OTHER) + _kf_clear_key(kf, "wifi", "mode"); + } + + ap->hidden = g_key_file_get_boolean(kf, "wifi", "hidden", NULL); + _kf_clear_key(kf, "wifi", "hidden"); + + if (!nd->access_points) + nd->access_points = g_hash_table_new(g_str_hash, g_str_equal); + g_hash_table_insert(nd->access_points, ap->ssid, ap); + + /* Last: handle passthrough for everything left in the keyfile + * Also, transfer backend_settings from netdef to AP */ + ap->backend_settings.nm.uuid = nd->backend_settings.nm.uuid; + ap->backend_settings.nm.name = nd->backend_settings.nm.name; + /* No need to clear nm.uuid & nm.name from def->backend_settings, + * as we have only one AP. */ + read_passthrough(kf, &ap->backend_settings.nm.passthrough); + } else { +only_passthrough: + /* Last: handle passthrough for everything left in the keyfile */ + read_passthrough(kf, &nd->backend_settings.nm.passthrough); + } + + g_key_file_free(kf); + return TRUE; +} diff -Nru netplan.io-0.101/src/parse-nm.h netplan.io-0.102/src/parse-nm.h --- netplan.io-0.101/src/parse-nm.h 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/src/parse-nm.h 2021-03-16 11:38:25.000000000 +0000 @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2021 Canonical, Ltd. + * Author: Lukas Märdian + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#define NETPLAN_NM_EMPTY_GROUP "_" + +gboolean netplan_parse_keyfile(const char* filename, GError** error); diff -Nru netplan.io-0.101/src/util.c netplan.io-0.102/src/util.c --- netplan.io-0.101/src/util.c 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/src/util.c 2021-03-16 11:38:25.000000000 +0000 @@ -22,6 +22,7 @@ #include #include "util.h" +#include "netplan.h" GHashTable* wifi_frequency_24; GHashTable* wifi_frequency_5; @@ -51,10 +52,12 @@ void g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix) { g_autofree char* full_path = NULL; + g_autofree char* path_suffix = NULL; g_autofree char* contents = g_string_free(s, FALSE); GError* error = NULL; - full_path = g_strjoin(NULL, rootdir ?: "", G_DIR_SEPARATOR_S, path, suffix, NULL); + path_suffix = g_strjoin(NULL, path, suffix, NULL); + full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL); safe_mkdir_p_dir(full_path); if (!g_file_set_contents(full_path, contents, -1, &error)) { /* the mkdir() just succeeded, there is no sensible @@ -195,3 +198,117 @@ return escaped; } + +gboolean +netplan_delete_connection(const char* id, const char* rootdir) +{ + g_autofree gchar* filename = NULL; + g_autofree gchar* del = NULL; + g_autoptr(GError) error = NULL; + NetplanNetDefinition* nd = NULL; + + /* parse all YAML files */ + if (!process_yaml_hierarchy(rootdir)) + return FALSE; // LCOV_EXCL_LINE + + netdefs = netplan_finish_parse(&error); + if (!netdefs) { + // LCOV_EXCL_START + g_fprintf(stderr, "netplan_delete_connection: %s\n", error->message); + return FALSE; + // LCOV_EXCL_STOP + } + + /* find filename for specified netdef ID */ + nd = g_hash_table_lookup(netdefs, id); + if (!nd) { + g_warning("netplan_delete_connection: Cannot delete %s, does not exist.", id); + return FALSE; + } + + filename = g_path_get_basename(nd->filename); + filename[strlen(filename) - 5] = '\0'; //stip ".yaml" suffix + del = g_strdup_printf("network.%s.%s=NULL", netplan_def_type_to_str[nd->type], id); + netplan_clear_netdefs(); + + /* TODO: refactor logic to actually be inside the library instead of spawning another process */ + const gchar *argv[] = { "/sbin/netplan", "set", del, "--origin-hint" , filename, NULL, NULL, NULL }; + if (rootdir) { + argv[5] = "--root-dir"; + argv[6] = rootdir; + } + if (getenv("TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("TEST_NETPLAN_CMD"); + return g_spawn_sync(NULL, (gchar**)argv, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL); +} + +gboolean +netplan_generate(const char* rootdir) +{ + /* TODO: refactor logic to actually be inside the library instead of spawning another process */ + const gchar *argv[] = { "/sbin/netplan", "generate", NULL , NULL, NULL }; + if (rootdir) { + argv[2] = "--root-dir"; + argv[3] = rootdir; + } + if (getenv("TEST_NETPLAN_CMD") != 0) + argv[0] = getenv("TEST_NETPLAN_CMD"); + return g_spawn_sync(NULL, (gchar**)argv, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL); +} + +/** + * Extract the netplan netdef ID from a NetworkManager connection profile (keyfile), + * generated by netplan. Used by the NetworkManager YAML backend. + */ +gchar* +netplan_get_id_from_nm_filename(const char* filename, const char* ssid) +{ + g_autofree gchar* escaped_ssid = NULL; + g_autofree gchar* suffix = NULL; + const char* nm_prefix = "/run/NetworkManager/system-connections/netplan-"; + const char* pos = g_strrstr(filename, nm_prefix); + const char* start = NULL; + const char* end = NULL; + gsize id_len = 0; + + if (!pos) + return NULL; + + if (ssid) { + escaped_ssid = g_uri_escape_string(ssid, NULL, TRUE); + suffix = g_strdup_printf("-%s.nmconnection", escaped_ssid); + end = g_strrstr(filename, suffix); + } else + end = g_strrstr(filename, ".nmconnection"); + + if (!end) + return NULL; + + /* Move pointer to start of netplan ID inside filename string */ + start = pos + strlen(nm_prefix); + id_len = end - start; + return g_strndup(start, id_len); +} + +/** + * Get the filename from which the given netdef has been parsed. + * @rootdir: ID of the netdef to be looked up + * @rootdir: parse files from this root directory + */ +gchar* +netplan_get_filename_by_id(const char* netdef_id, const char* rootdir) +{ + gchar* filename = NULL; + netplan_clear_netdefs(); + if (!process_yaml_hierarchy(rootdir)) + return NULL; // LCOV_EXCL_LINE + GHashTable* netdefs = netplan_finish_parse(NULL); + if (!netdefs) + return NULL; + NetplanNetDefinition* nd = g_hash_table_lookup(netdefs, netdef_id); + if (!nd) + return NULL; + filename = g_strdup(nd->filename); + netplan_clear_netdefs(); + return filename; +} diff -Nru netplan.io-0.101/src/util.h netplan.io-0.102/src/util.h --- netplan.io-0.101/src/util.h 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/src/util.h 2021-03-16 11:38:25.000000000 +0000 @@ -31,5 +31,9 @@ int wifi_get_freq5(int channel); gchar* systemd_escape(char* string); +gboolean netplan_delete_connection(const char* id, const char* rootdir); +gboolean netplan_generate(const char* rootdir); +gchar* netplan_get_id_from_nm_filename(const char* filename, const char* ssid); +gchar* netplan_get_filename_by_id(const char* netdef_id, const char* rootdir); #define OPENVSWITCH_OVS_VSCTL "/usr/bin/ovs-vsctl" diff -Nru netplan.io-0.101/src/validation.c netplan.io-0.102/src/validation.c --- netplan.io-0.101/src/validation.c 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/src/validation.c 2021-03-16 11:38:25.000000000 +0000 @@ -205,6 +205,8 @@ return yaml_error(node, error, "%s: missing 'local' property for tunnel", nd->id); if (!nd->tunnel.remote_ip) return yaml_error(node, error, "%s: missing 'remote' property for tunnel", nd->id); + if (nd->tunnel.ttl && nd->tunnel.ttl > 255) + return yaml_error(node, error, "%s: 'ttl' property for tunnel must be in range [1...255]", nd->id); switch(nd->tunnel.mode) { case NETPLAN_TUNNEL_MODE_IPIP6: @@ -340,6 +342,9 @@ // LCOV_EXCL_STOP } + if (nd->type == NETPLAN_DEF_TYPE_NM && (!nd->backend_settings.nm.passthrough || !g_datalist_get_data(&nd->backend_settings.nm.passthrough, "connection.type"))) + return yaml_error(node, error, "%s: network type 'nm-devices:' needs to provide a 'connection.type' via passthrough", nd->id); + valid = TRUE; netdef_grammar_error: diff -Nru netplan.io-0.101/tests/cli.py netplan.io-0.102/tests/cli.py --- netplan.io-0.101/tests/cli.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/cli.py 2021-03-16 11:38:25.000000000 +0000 @@ -72,7 +72,7 @@ stderr=subprocess.PIPE) (out, err) = p.communicate() self.assertEqual(out, b'') - self.assertEqual(os.listdir(self.workdir.name), []) + self.assertEqual(os.listdir(self.workdir.name), ['run']) def test_with_empty_config(self): c = os.path.join(self.workdir.name, 'etc', 'netplan') diff -Nru netplan.io-0.101/tests/dbus/test_dbus.py netplan.io-0.102/tests/dbus/test_dbus.py --- netplan.io-0.101/tests/dbus/test_dbus.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/dbus/test_dbus.py 2021-03-16 11:38:25.000000000 +0000 @@ -20,6 +20,8 @@ import unittest import time +from tests.test_utils import MockCmd + rootdir = os.path.dirname(os.path.dirname( os.path.dirname(os.path.abspath(__file__)))) exe_cli = [os.path.join(rootdir, 'src', 'netplan.script')] @@ -31,63 +33,6 @@ NETPLAN_DBUS_CMD = os.path.join(os.path.dirname(__file__), "..", "..", "netplan-dbus") -class MockCmd: - """MockCmd will mock a given command name and capture all calls to it""" - - def __init__(self, name): - self._tmp = tempfile.TemporaryDirectory() - self.name = name - self.path = os.path.join(self._tmp.name, name) - self.call_log = os.path.join(self._tmp.name, "call.log") - with open(self.path, "w") as fp: - fp.write("""#!/bin/bash -printf "%%s" "$(basename "$0")" >> %(log)s -printf '\\0' >> %(log)s - -for arg in "$@"; do - printf "%%s" "$arg" >> %(log)s - printf '\\0' >> %(log)s -done - -printf '\\0' >> %(log)s -""" % {'log': self.call_log}) - os.chmod(self.path, 0o755) - - def calls(self): - """ - calls() returns the calls to the given mock command in the form of - [ ["cmd", "call1-arg1"], ["cmd", "call2-arg1"], ... ] - """ - with open(self.call_log) as fp: - b = fp.read() - calls = [] - for raw_call in b.rstrip("\0\0").split("\0\0"): - call = raw_call.rstrip("\0") - calls.append(call.split("\0")) - return calls - - def set_output(self, output): - with open(self.path, "a") as fp: - fp.write("cat << EOF\n%s\nEOF" % output) - - def set_timeout(self, timeout=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): @@ -131,6 +76,7 @@ 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 +144,8 @@ ]) 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) @@ -296,13 +242,14 @@ "io.netplan.Netplan", "/io/netplan/Netplan/config/{}".format(cid), "io.netplan.Netplan.Config", - "Set", "ss", "ethernets.eth42.dhcp6=true", "testfile", + "Set", "ss", "ethernets.eth42.dhcp6=true", "", ] out = subprocess.check_output(BUSCTL_NETPLAN_CMD) self.assertEqual(b'b true\n', out) + print(self.mock_netplan_cmd.calls(), flush=True) self.assertEquals(self.mock_netplan_cmd.calls(), [[ "netplan", "set", "ethernets.eth42.dhcp6=true", - "--origin-hint=testfile", "--root-dir={}".format(tmpdir) + "--root-dir={}".format(tmpdir) ]]) def test_netplan_dbus_config_get(self): @@ -339,6 +286,8 @@ ] 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 +315,7 @@ 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 +328,8 @@ 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 +346,7 @@ "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 +375,7 @@ ] 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 +392,10 @@ 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 +435,14 @@ 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 +458,14 @@ 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 +476,13 @@ "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 +521,7 @@ "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 +626,7 @@ ]) 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 +660,7 @@ 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.101/tests/generator/test_args.py netplan.io-0.102/tests/generator/test_args.py --- netplan.io-0.101/tests/generator/test_args.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_args.py 2021-03-16 11:38:25.000000000 +0000 @@ -27,7 +27,7 @@ def test_no_files(self): subprocess.check_call([exe_generate, '--root-dir', self.workdir.name]) - self.assertEqual(os.listdir(self.workdir.name), []) + self.assertEqual(os.listdir(self.workdir.name), ['run']) self.assert_nm_udev(None) def test_no_configs(self): @@ -113,7 +113,8 @@ eth0: dhcp4: true''') err = self.generate('', extra_args=['--root-dir', '/proc/foo', conf], expect_fail=True) - self.assertIn('cannot create directory /proc/foo/run/systemd/network', err) + # can be /proc/foor/run/systemd/{network,system} + self.assertIn('cannot create directory /proc/foo/run/systemd/', err) def test_systemd_generator(self): conf = os.path.join(self.confdir, 'a.yaml') diff -Nru netplan.io-0.101/tests/generator/test_auth.py netplan.io-0.102/tests/generator/test_auth.py --- netplan.io-0.101/tests/generator/test_auth.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_auth.py 2021-03-16 11:38:25.000000000 +0000 @@ -291,9 +291,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -313,9 +310,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -335,9 +329,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -362,9 +353,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -389,9 +377,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -416,9 +401,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -447,9 +429,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -465,9 +444,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto diff -Nru netplan.io-0.101/tests/generator/test_bridges.py netplan.io-0.102/tests/generator/test_bridges.py --- netplan.io-0.101/tests/generator/test_bridges.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_bridges.py 2021-03-16 11:38:25.000000000 +0000 @@ -35,6 +35,9 @@ self.assert_networkd({'br0.network': '''[Match] Name=br0 +[Link] +MACAddress=00:01:02:03:04:05 + [Network] DHCP=ipv4 LinkLocalAddressing=ipv6 @@ -358,7 +361,7 @@ type=bridge interface-name=br0 -[802-3-ethernet] +[ethernet] cloned-mac-address=00:01:02:03:04:05 [ipv4] diff -Nru netplan.io-0.101/tests/generator/test_common.py netplan.io-0.102/tests/generator/test_common.py --- netplan.io-0.101/tests/generator/test_common.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_common.py 2021-03-16 11:38:25.000000000 +0000 @@ -390,6 +390,41 @@ 'br0.network': ND_EMPTY % ('br0', 'ipv6'), 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n'}) + def test_bond_arp_ip_targets_multi_pass(self): + self.generate('''network: + bonds: + bond0: + interfaces: + - eno1 + parameters: + arp-ip-targets: + - 10.10.10.10 + - 20.20.20.20 + ethernets: + eno1: {} + version: 2''') + self.assert_networkd({'bond0.netdev': '''[NetDev] +Name=bond0 +Kind=bond + +[Bond] +ARPIPTargets=10.10.10.10 20.20.20.20 +''', + 'bond0.network': '''[Match] +Name=bond0 + +[Network] +LinkLocalAddressing=ipv6 +ConfigureWithoutCarrier=yes +''', + 'eno1.network': '''[Match] +Name=eno1 + +[Network] +LinkLocalAddressing=no +Bond=bond0 +'''}) + def test_dhcp_critical_true(self): self.generate('''network: version: 2 @@ -705,7 +740,7 @@ type=bond interface-name=bond0 -[802-3-ethernet] +[ethernet] mtu=9000 [ipv4] @@ -723,8 +758,6 @@ [ethernet] wake-on-lan=0 - -[802-3-ethernet] mtu=1280 [ipv4] diff -Nru netplan.io-0.101/tests/generator/test_errors.py netplan.io-0.102/tests/generator/test_errors.py --- netplan.io-0.101/tests/generator/test_errors.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_errors.py 2021-03-16 11:38:25.000000000 +0000 @@ -367,6 +367,17 @@ ipv6-address-token: INVALID''', expect_fail=True) self.assertIn("invalid ipv6-address-token 'INVALID'", err) + def test_nm_devices_missing_passthrough(self): + err = self.generate('''network: + version: 2 + renderer: NetworkManager + nm-devices: + engreen: + networkmanager: + passthrough: + connection.uuid: "123456"''', expect_fail=True) + self.assertIn("engreen: network type 'nm-devices:' needs to provide a 'connection.type' via passthrough", err) + def test_invalid_address_node_type(self): err = self.generate('''network: version: 2 @@ -540,6 +551,36 @@ addresses: - 192.168.14.2/24 - 2001:FFfe::1/64''', expect_fail=True) + + self.assertIn("invalid unsigned int value '-1'", err) + + def test_device_bad_route_congestion_window(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: 10.1.1.1 + congestion-window: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) + + self.assertIn("invalid unsigned int value '-1'", err) + + def test_device_bad_route_advertised_receive_window(self): + err = self.generate('''network: + version: 2 + ethernets: + engreen: + routes: + - to: 10.10.0.0/16 + via: 10.1.1.1 + advertised-receive-window: -1 + addresses: + - 192.168.14.2/24 + - 2001:FFfe::1/64''', expect_fail=True) self.assertIn("invalid unsigned int value '-1'", err) diff -Nru netplan.io-0.101/tests/generator/test_ethernets.py netplan.io-0.102/tests/generator/test_ethernets.py --- netplan.io-0.101/tests/generator/test_ethernets.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_ethernets.py 2021-03-16 11:38:25.000000000 +0000 @@ -225,8 +225,8 @@ 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) @@ -354,8 +354,6 @@ [ethernet] wake-on-lan=0 - -[802-3-ethernet] mtu=1280 [ipv4] @@ -442,13 +440,7 @@ 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 @@ -457,8 +449,6 @@ [ethernet] wake-on-lan=0 - -[802-3-ethernet] cloned-mac-address=00:01:02:03:04:05 [ipv4] @@ -575,8 +565,6 @@ [ethernet] wake-on-lan=0 - -[802-3-ethernet] mac-address=11:22:33:44:55:66 [ipv4] @@ -715,8 +703,6 @@ [ethernet] wake-on-lan=0 - -[802-3-ethernet] mac-address=00:11:22:33:44:55 [ipv4] diff -Nru netplan.io-0.101/tests/generator/test_modems.py netplan.io-0.102/tests/generator/test_modems.py --- netplan.io-0.101/tests/generator/test_modems.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_modems.py 2021-03-16 11:38:25.000000000 +0000 @@ -59,9 +59,6 @@ username=test-user number=#666 -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -86,9 +83,6 @@ [gsm] auto-config=true -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -118,9 +112,6 @@ number=*99# pin=1234 -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -145,9 +136,6 @@ [gsm] apn=internet -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -176,9 +164,6 @@ password=some-pass username=some-user -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -204,9 +189,6 @@ auto-config=true device-id=test -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -232,9 +214,6 @@ auto-config=true network-id=test -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -260,9 +239,6 @@ auto-config=true pin=1234 -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -288,9 +264,6 @@ auto-config=true sim-id=test -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -316,9 +289,6 @@ auto-config=true sim-operator-id=test -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -361,8 +331,32 @@ sim-id=89148000000060671234 sim-operator-id=310260 -[ethernet] -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 +uuid=b22d8f0f-3f34-46bd-ac28-801fa87f1eb6 +interface-name=mobilephone + +[gsm] +auto-config=true [ipv4] method=link-local @@ -372,3 +366,60 @@ '''}) self.assert_networkd({}) self.assert_nm_udev(None) + + def test_modem_nm_integration_gsm_cdma(self): + self.generate('''network: + version: 2 + modems: + NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3: + renderer: NetworkManager + match: {} + apn: internet2.voicestream.com + networkmanager: + uuid: a08c5805-7cf5-43f7-afb9-12cb30f6eca3 + name: "T-Mobile Funkadelic 2" + passthrough: + connection.type: "bluetooth" + gsm.apn: "internet2.voicestream.com" + gsm.device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" + gsm.username: "george.clinton.again" + gsm.sim-operator-id: "310260" + gsm.pin: "123456" + gsm.sim-id: "89148000000060671234" + gsm.password: "parliament2" + gsm.network-id: "254098" + ipv4.method: "auto" + ipv6.method: "auto"''') + self.assert_nm({'NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3': '''[connection] +id=T-Mobile Funkadelic 2 +#Netplan: passthrough override +type=bluetooth +uuid=a08c5805-7cf5-43f7-afb9-12cb30f6eca3 + +[gsm] +apn=internet2.voicestream.com +#Netplan: passthrough setting +device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 +#Netplan: passthrough setting +username=george.clinton.again +#Netplan: passthrough setting +sim-operator-id=310260 +#Netplan: passthrough setting +pin=123456 +#Netplan: passthrough setting +sim-id=89148000000060671234 +#Netplan: passthrough setting +password=parliament2 +#Netplan: passthrough setting +network-id=254098 + +[ipv4] +#Netplan: passthrough override +method=auto + +[ipv6] +#Netplan: passthrough override +method=auto +'''}) + self.assert_networkd({}) + self.assert_nm_udev(None) diff -Nru netplan.io-0.101/tests/generator/test_ovs.py netplan.io-0.102/tests/generator/test_ovs.py --- netplan.io-0.101/tests/generator/test_ovs.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_ovs.py 2021-03-16 11:38:25.000000000 +0000 @@ -293,7 +293,7 @@ openvswitch: lacp: passive ''', expect_fail=True) - self.assertIn("Key 'lacp' is only valid for iterface type 'openvswitch bond'", err) + self.assertIn("Key 'lacp' is only valid for interface type 'openvswitch bond'", err) def test_bond_mode_implicit_params(self): self.generate('''network: @@ -497,7 +497,7 @@ openvswitch: fail-mode: glorious ''', expect_fail=True) - self.assertIn("Key 'fail-mode' is only valid for iterface type 'openvswitch bridge'", err) + self.assertIn("Key 'fail-mode' is only valid for interface type 'openvswitch bridge'", err) def test_rstp_non_bridge(self): err = self.generate('''network: @@ -507,7 +507,7 @@ openvswitch: rstp: true ''', expect_fail=True) - self.assertIn("Key is only valid for iterface type 'openvswitch bridge'", err) + self.assertIn("Key is only valid for interface type 'openvswitch bridge'", err) def test_bridge_set_protocols(self): self.generate('''network: @@ -548,7 +548,7 @@ openvswitch: protocols: [OpenFlow10, OpenFlow15] ''', expect_fail=True) - self.assertIn("Key 'protocols' is only valid for iterface type 'openvswitch bridge'", err) + self.assertIn("Key 'protocols' is only valid for interface type 'openvswitch bridge'", err) def test_bridge_controller(self): self.generate('''network: @@ -651,7 +651,7 @@ controller: connection-mode: in-band ''', expect_fail=True) - self.assertIn("Key 'controller.connection-mode' is only valid for iterface type 'openvswitch bridge'", err) + self.assertIn("Key 'controller.connection-mode' is only valid for interface type 'openvswitch bridge'", err) self.assert_ovs({}) self.assert_networkd({}) @@ -664,7 +664,7 @@ controller: addresses: [unix:/some/socket] ''', expect_fail=True) - self.assertIn("Key 'controller.addresses' is only valid for iterface type 'openvswitch bridge'", err) + self.assertIn("Key 'controller.addresses' is only valid for interface type 'openvswitch bridge'", err) self.assert_ovs({}) self.assert_networkd({}) @@ -700,7 +700,7 @@ ''', expect_fail=True) self.assertIn("ERROR: openvswitch bridge controller target 'ssl:10.10.10.1' needs SSL configuration, but global \ 'openvswitch.ssl' settings are not set", err) - self.assert_ovs({}) + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({}) def test_global_ports(self): @@ -711,7 +711,7 @@ - [patch0-1, patch1-0] ''', expect_fail=True) self.assertIn('patch0-1: OpenVSwitch patch port needs to be assigned to a bridge/bond', err) - self.assert_ovs({}) + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({}) def test_few_ports(self): @@ -987,7 +987,7 @@ openvswitch: {} ''', expect_fail=True) self.assertIn('eth0: This device type is not supported with the OpenVSwitch backend', err) - self.assert_ovs({}) + self.assert_ovs({'cleanup.service': OVS_CLEANUP % {'iface': 'cleanup'}}) self.assert_networkd({}) def test_bridge_non_ovs_bond(self): diff -Nru netplan.io-0.101/tests/generator/test_passthrough.py netplan.io-0.102/tests/generator/test_passthrough.py --- netplan.io-0.101/tests/generator/test_passthrough.py 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_passthrough.py 2021-03-16 11:38:25.000000000 +0000 @@ -0,0 +1,285 @@ +# +# Tests for passthrough config generated via netplan +# +# Copyright (C) 2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from .base import TestBase + + +# No passthrough mode (yet) for systemd-networkd +class TestNetworkd(TestBase): + pass + + +class TestNetworkManager(TestBase): + + def test_passthrough_basic(self): + self.generate('''network: + version: 2 + ethernets: + NM-87749f1d-334f-40b2-98d4-55db58965f5f: + renderer: NetworkManager + match: {} + networkmanager: + uuid: 87749f1d-334f-40b2-98d4-55db58965f5f + name: some NM id + passthrough: + connection.uuid: 87749f1d-334f-40b2-98d4-55db58965f5f + connection.type: ethernet + connection.permissions:''') + + self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f': '''[connection] +id=some NM id +type=ethernet +uuid=87749f1d-334f-40b2-98d4-55db58965f5f +#Netplan: passthrough setting +permissions= + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_passthrough_wifi(self): + self.generate('''network: + version: 2 + wifis: + NM-87749f1d-334f-40b2-98d4-55db58965f5f: + renderer: NetworkManager + match: {} + access-points: + "SOME-SSID": + networkmanager: + uuid: 87749f1d-334f-40b2-98d4-55db58965f5f + name: myid with spaces + passthrough: + connection.permissions: + wifi.ssid: SOME-SSID + "OTHER-SSID": + hidden: true''') + + self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f-SOME-SSID': '''[connection] +id=myid with spaces +type=wifi +uuid=87749f1d-334f-40b2-98d4-55db58965f5f +#Netplan: passthrough setting +permissions= + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=SOME-SSID +mode=infrastructure +''', + 'NM-87749f1d-334f-40b2-98d4-55db58965f5f-OTHER-SSID': '''[connection] +id=netplan-NM-87749f1d-334f-40b2-98d4-55db58965f5f-OTHER-SSID +type=wifi + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=OTHER-SSID +mode=infrastructure +hidden=true +'''}) + + def test_passthrough_type_nm_devices(self): + self.generate('''network: + nm-devices: + NM-87749f1d-334f-40b2-98d4-55db58965f5f: + renderer: NetworkManager + match: {} + networkmanager: + passthrough: + connection.uuid: 87749f1d-334f-40b2-98d4-55db58965f5f + connection.type: dummy''') + + self.assert_nm({'NM-87749f1d-334f-40b2-98d4-55db58965f5f': '''[connection] +id=netplan-NM-87749f1d-334f-40b2-98d4-55db58965f5f +#Netplan: passthrough setting +uuid=87749f1d-334f-40b2-98d4-55db58965f5f +#Netplan: passthrough setting +type=dummy + +[ipv4] +method=link-local + +[ipv6] +method=ignore +'''}) + + def test_passthrough_dotted_group(self): + self.generate('''network: + nm-devices: + dotted-group-test: + renderer: NetworkManager + match: {} + networkmanager: + passthrough: + connection.type: "wireguard" + wireguard-peer.some-key.endpoint: 1.2.3.4''') + + self.assert_nm({'dotted-group-test': '''[connection] +id=netplan-dotted-group-test +#Netplan: passthrough setting +type=wireguard + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wireguard-peer.some-key] +#Netplan: passthrough setting +endpoint=1.2.3.4 +'''}) + + def test_passthrough_dotted_key(self): + self.generate('''network: + ethernets: + dotted-key-test: + renderer: NetworkManager + match: {} + networkmanager: + passthrough: + tc.qdisc.root: something + tc.qdisc.fff1: ":abc" + tc.filters.test: "test"''') + + self.assert_nm({'dotted-key-test': '''[connection] +id=netplan-dotted-key-test +type=ethernet + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[tc] +#Netplan: passthrough setting +qdisc.root=something +#Netplan: passthrough setting +qdisc.fff1=:abc +#Netplan: passthrough setting +filters.test=test +'''}) + + def test_passthrough_unsupported_setting(self): + self.generate('''network: + wifis: + test: + renderer: NetworkManager + match: {} + access-points: + "SOME-SSID": # implicit "mode: infrasturcutre" + networkmanager: + passthrough: + wifi.mode: "mesh"''') + + self.assert_nm({'test-SOME-SSID': '''[connection] +id=netplan-test-SOME-SSID +type=wifi + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[wifi] +ssid=SOME-SSID +#Netplan: passthrough override +mode=mesh +'''}) + + def test_passthrough_empty_group(self): + self.generate('''network: + ethernets: + test: + renderer: NetworkManager + match: {} + networkmanager: + passthrough: + proxy._: ""''') + + self.assert_nm({'test': '''[connection] +id=netplan-test +type=ethernet + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=link-local + +[ipv6] +method=ignore + +[proxy] +'''}) + + def test_passthrough_interface_rename_existing_id(self): + self.generate('''network: + version: 2 + renderer: NetworkManager + ethernets: + # This is the original netdef, generating "netplan-eth0.nmconnection" + eth0: + dhcp4: true + # This is the override netdef, modifying match.original_name (i.e. interface-name) + # it should still generate a "netplan-eth0.nmconnection" file (not netplan-eth33.nmconnection). + eth0: + renderer: NetworkManager + match: + name: "eth33" + networkmanager: + uuid: 626dd384-8b3d-3690-9511-192b2c79b3fd + name: "netplan-eth0" +''') + + self.assert_nm({'eth0': '''[connection] +id=netplan-eth0 +type=ethernet +uuid=626dd384-8b3d-3690-9511-192b2c79b3fd +interface-name=eth33 + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=auto + +[ipv6] +method=ignore +'''}) diff -Nru netplan.io-0.101/tests/generator/test_routing.py netplan.io-0.102/tests/generator/test_routing.py --- netplan.io-0.101/tests/generator/test_routing.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_routing.py 2021-03-16 11:38:25.000000000 +0000 @@ -369,6 +369,56 @@ MTUBytes=1500 '''}) + def test_route_v4_congestion_window(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + congestion-window: 16 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +InitialCongestionWindow=16 +'''}) + + def test_route_v4_advertised_receive_window(self): + self.generate('''network: + version: 2 + ethernets: + engreen: + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.14.20 + advertised-receive-window: 16 + ''') + + self.assert_networkd({'engreen.network': '''[Match] +Name=engreen + +[Network] +LinkLocalAddressing=ipv6 +Address=192.168.14.2/24 + +[Route] +Destination=10.10.10.0/24 +Gateway=192.168.14.20 +InitialAdvertisedReceiveWindow=16 +'''}) + def test_route_v6_single(self): self.generate('''network: version: 2 @@ -929,6 +979,72 @@ [ipv6] method=ignore +'''}) + self.assert_networkd({}) + + def test_route_congestion_window(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + congestion-window: 16 + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.1.20 +route1_options=initcwnd=16 + +[ipv6] +method=ignore +'''}) + self.assert_networkd({}) + + def test_route_advertised_receive_window(self): + out = self.generate('''network: + version: 2 + ethernets: + engreen: + renderer: NetworkManager + addresses: ["192.168.14.2/24"] + routes: + - to: 10.10.10.0/24 + via: 192.168.1.20 + advertised-receive-window: 16 + ''') + self.assertEqual('', out) + + self.assert_nm({'engreen': '''[connection] +id=netplan-engreen +type=ethernet +interface-name=engreen + +[ethernet] +wake-on-lan=0 + +[ipv4] +method=manual +address1=192.168.14.2/24 +route1=10.10.10.0/24,192.168.1.20 +route1_options=initrwnd=16 + +[ipv6] +method=ignore '''}) self.assert_networkd({}) diff -Nru netplan.io-0.101/tests/generator/test_tunnels.py netplan.io-0.102/tests/generator/test_tunnels.py --- netplan.io-0.101/tests/generator/test_tunnels.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_tunnels.py 2021-03-16 11:38:25.000000000 +0000 @@ -19,7 +19,7 @@ from .base import TestBase, ND_WITHIPGW, ND_EMPTY, NM_WG, ND_WG -def prepare_config_for_mode(renderer, mode, key=None): +def prepare_config_for_mode(renderer, mode, key=None, ttl=None): config = """network: version: 2 renderer: {} @@ -36,15 +36,16 @@ local_ip = "10.10.10.10" remote_ip = "20.20.20.20" + append_ttl = '\n ttl: {}'.format(ttl) if ttl else '' config += """ tunnels: tun0: mode: {} local: {} - remote: {} + remote: {}{} addresses: [ 15.15.15.15/24 ] gateway4: 20.20.20.21 -""".format(mode, local_ip, remote_ip) +""".format(mode, local_ip, remote_ip, append_ttl) # Handle key/keys as str or dict as required by the test if type(key) is str: @@ -359,7 +360,7 @@ endpoint=1.2.3.4:5 preshared-key=7voRZ/ojfXgfPOlswo3Lpma1RJq7qijIEEUEMShQFV8= preshared-key-flags=0 -allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24''')}) +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) def test_simple_multi_pass(self): """[wireguard] Validate generation of a wireguard config, which is parsed multiple times""" @@ -397,7 +398,7 @@ [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] persistent-keepalive=23 endpoint=1.2.3.4:5 -allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24 +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24; [ipv4] method=manual @@ -427,7 +428,7 @@ 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '1.2.3.4:5'}, { - 'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=', + 'public-key': 'M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5=', 'allowed-ips': '[0.0.0.0/0, "2001:fe:ad:de:ad:be:ef:1/24"]', 'keepalive': 23, 'endpoint': '1.2.3.4:5'}], renderer=self.backend) @@ -441,7 +442,7 @@ Endpoint=1.2.3.4:5 [WireGuardPeer] -PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4= +PublicKey=M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5= AllowedIPs=0.0.0.0/0,2001:fe:ad:de:ad:be:ef:1/24 PersistentKeepalive=23 Endpoint=1.2.3.4:5'''), @@ -452,12 +453,12 @@ [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] persistent-keepalive=23 endpoint=1.2.3.4:5 -allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24 +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24; -[wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] +[wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG5=] persistent-keepalive=23 endpoint=1.2.3.4:5 -allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24''')}) +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) def test_privatekeyfile(self): """[wireguard] Validate generation of another simple wireguard config""" @@ -504,7 +505,7 @@ [wireguard-peer.M9nt4YujIOmNrRmpIRTmYSfMdrpvE7u6WkG8FY8WjG4=] persistent-keepalive=23 endpoint=[2001:fe:ad:de:ad:be:ef:11]:5 -allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24''')}) +allowed-ips=0.0.0.0/0;2001:fe:ad:de:ad:be:ef:1/24;''')}) # Execute the _CommonParserErrors only for one backend, to spare some test cycles @@ -744,7 +745,7 @@ def test_ipip(self): """[networkd] Validate generation of IPIP tunnels""" - config = prepare_config_for_mode('networkd', 'ipip') + config = prepare_config_for_mode('networkd', 'ipip', ttl=64) self.generate(config) self.assert_networkd({'tun0.netdev': '''[NetDev] Name=tun0 @@ -754,6 +755,7 @@ Independent=true Local=10.10.10.10 Remote=20.20.20.20 +TTL=64 ''', 'tun0.network': '''[Match] Name=tun0 @@ -1066,7 +1068,7 @@ def test_ipip(self): """[NetworkManager] Validate generation of IPIP tunnels""" - config = prepare_config_for_mode('NetworkManager', 'ipip') + config = prepare_config_for_mode('NetworkManager', 'ipip', ttl=64) self.generate(config) self.assert_nm({'tun0': '''[connection] id=netplan-tun0 @@ -1077,6 +1079,7 @@ mode=1 local=10.10.10.10 remote=20.20.20.20 +ttl=64 [ipv4] method=manual @@ -1260,6 +1263,20 @@ out = self.generate(config, expect_fail=True) self.assertIn("Error in network definition: tun0: missing 'remote' property for tunnel", out) + def test_invalid_ttl(self): + """Fail if TTL not in range [1...255]""" + config = '''network: + version: 2 + tunnels: + tun0: + mode: ipip + local: 20.20.20.20 + remote: 10.10.10.10 + ttl: 300 +''' + out = self.generate(config, expect_fail=True) + self.assertIn("Error in network definition: tun0: 'ttl' property for tunnel must be in range [1...255]", out) + def test_wrong_local_ip_for_mode_v4(self): """Show an error when an IPv6 local addr is used for an IPv4 tunnel mode""" config = '''network: diff -Nru netplan.io-0.101/tests/generator/test_vlans.py netplan.io-0.102/tests/generator/test_vlans.py --- netplan.io-0.101/tests/generator/test_vlans.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_vlans.py 2021-03-16 11:38:25.000000000 +0000 @@ -62,7 +62,8 @@ 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] @@ -231,8 +232,6 @@ [ethernet] wake-on-lan=0 - -[802-3-ethernet] mac-address=11:22:33:44:55:66 [ipv4] diff -Nru netplan.io-0.101/tests/generator/test_wifis.py netplan.io-0.102/tests/generator/test_wifis.py --- netplan.io-0.101/tests/generator/test_wifis.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/generator/test_wifis.py 2021-03-16 11:38:25.000000000 +0000 @@ -265,7 +265,7 @@ password: "c0mpany1" mode: ap dhcp4: yes''', expect_fail=True) - self.assertIn('networkd does not support wifi in access point mode', err) + self.assertIn('wl0: workplace: networkd does not support this wifi mode', err) def test_wifi_wowlan(self): self.generate('''network: @@ -382,9 +382,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -407,9 +404,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -432,9 +426,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -455,9 +446,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -477,9 +465,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -495,9 +480,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=auto @@ -527,21 +509,16 @@ id=netplan-all-workplace type=wifi -[ethernet] -wake-on-lan=0 - -[802-11-wireless] +[wifi] mac-address=11:22:33:44:55:66 +ssid=workplace +mode=infrastructure [ipv4] method=link-local [ipv6] method=ignore - -[wifi] -ssid=workplace -mode=infrastructure '''}) def test_wifi_match_all(self): @@ -558,9 +535,6 @@ id=netplan-all-workplace type=wifi -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -588,9 +562,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=shared @@ -623,9 +594,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local @@ -652,21 +620,16 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - -[802-11-wireless] +[wifi] wake-on-wlan=330 +ssid=homenet +mode=infrastructure [ipv4] method=link-local [ipv6] method=ignore - -[wifi] -ssid=homenet -mode=infrastructure '''}) def test_wifi_wowlan_default(self): @@ -684,9 +647,6 @@ type=wifi interface-name=wl0 -[ethernet] -wake-on-lan=0 - [ipv4] method=link-local diff -Nru netplan.io-0.101/tests/integration/base.py netplan.io-0.102/tests/integration/base.py --- netplan.io-0.101/tests/integration/base.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/base.py 2021-03-16 11:38:25.000000000 +0000 @@ -30,6 +30,7 @@ import unittest import shutil import gi +import glob # make sure we point to libnetplan properly. os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))}) @@ -51,39 +52,35 @@ class IntegrationTestsBase(unittest.TestCase): '''Common functionality for network test cases - setUp() creates two test wlan devices, one for a simulated access point - (self.dev_w_ap), the other for a simulated client device - (self.dev_w_client), and two test ethernet devices (self.dev_e_{ap,client} - and self.dev_e2_{ap,client}. + setUp() creates two test ethernet devices (self.dev_e_{ap,client} and + self.dev_e2_{ap,client}. - Each test should call self.setup_ap() or self.setup_eth() with the desired - configuration. + Each test should call self.setup_eth() with the desired configuration. ''' @classmethod def setUpClass(klass): + shutil.rmtree('/etc/netplan', ignore_errors=True) + os.makedirs('/etc/netplan', exist_ok=True) + # Try to keep autopkgtest's management network (eth0/ens3) up and + # configured. It should be running all the time, independently of netplan + os.makedirs('/etc/systemd/network', exist_ok=True) + with open('/etc/systemd/network/20-wired.network', 'w') as f: + f.write('[Match]\nName=eth0 en*\n\n[Network]\nDHCP=ipv4') + # ensure NM can manage our fake eths os.makedirs('/run/udev/rules.d', exist_ok=True) - with open('/run/udev/rules.d/99-nm-veth-test.rules', 'w') as f: f.write('ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="eth42|eth43", ENV{NM_UNMANAGED}="0"\n') subprocess.check_call(['udevadm', 'control', '--reload']) - # ensure we have this so that iw works - try: - subprocess.check_call(['modprobe', 'cfg80211']) - # set regulatory domain "EU", so that we can use 80211.a 5 GHz channels - out = subprocess.check_output(['iw', 'reg', 'get'], universal_newlines=True) - m = re.match(r'^(?:global\n)?country (\S+):', out) - assert m - klass.orig_country = m.group(1) - subprocess.check_call(['iw', 'reg', 'set', 'EU']) - except Exception: - raise unittest.SkipTest("cfg80211 (wireless) is unavailable, can't test") - + os.makedirs('/etc/NetworkManager/conf.d', exist_ok=True) + with open('/etc/NetworkManager/conf.d/99-test-ignore.conf', 'w') as f: + f.write('[keyfile]\nunmanaged-devices+=interface-name:eth0,interface-name:en*,interface-name:veth42,interface-name:veth43') + subprocess.check_call(['netplan', 'apply']) + subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) @classmethod def tearDownClass(klass): - subprocess.check_call(['iw', 'reg', 'set', klass.orig_country]) try: os.remove('/run/NetworkManager/conf.d/test-blacklist.conf') except FileNotFoundError: @@ -103,17 +100,22 @@ shutil.rmtree('/etc/netplan', ignore_errors=True) shutil.rmtree('/run/NetworkManager', ignore_errors=True) shutil.rmtree('/run/systemd/network', ignore_errors=True) + for f in glob.glob('/run/systemd/system/netplan-*'): + os.remove(f) + for f in glob.glob('/run/systemd/system/**/netplan-*'): + os.remove(f) + subprocess.call(['systemctl', 'daemon-reload']) try: os.remove('/run/systemd/generator/netplan.stamp') except FileNotFoundError: pass + # Keep the management network (eth0/ens3 from 20-wired.network) up + subprocess.check_call(['systemctl', 'restart', 'systemd-networkd']) @classmethod def create_devices(klass): - '''Create Access Point and Client devices with mac80211_hwsim and veth''' + '''Create Access Point and Client devices with veth''' - if os.path.exists('/sys/module/mac80211_hwsim'): - raise SystemError('mac80211_hwsim module already loaded') if os.path.exists('/sys/class/net/eth42'): raise SystemError('eth42 interface already exists') @@ -124,15 +126,20 @@ klass.dev_e_client = 'eth42' klass.dev_e_ap_ip4 = '192.168.5.1/24' klass.dev_e_ap_ip6 = '2600::1/64' - out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth42'], - universal_newlines=True) - klass.dev_e_client_mac = out.split()[2] subprocess.check_call(['ip', 'link', 'add', 'name', 'eth43', 'type', 'veth', 'peer', 'name', 'veth43']) klass.dev_e2_ap = 'veth43' klass.dev_e2_client = 'eth43' klass.dev_e2_ap_ip4 = '192.168.6.1/24' klass.dev_e2_ap_ip6 = '2601::1/64' + # Creation of the veths introduces a race with newer versions of + # systemd, as it will change the initial MAC address after the device + # was created and networkd took control. Give it some time, so we read + # the correct MAC address + time.sleep(0.1) + out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth42'], + universal_newlines=True) + klass.dev_e_client_mac = out.split()[2] out = subprocess.check_output(['ip', '-br', 'link', 'show', 'dev', 'eth43'], universal_newlines=True) klass.dev_e2_client_mac = out.split()[2] @@ -143,28 +150,6 @@ with open('/run/NetworkManager/conf.d/11-globally-managed-devices.conf', 'w') as f: f.write('[keyfile]\nunmanaged-devices=') - # create virtual wlan devs - before_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) - subprocess.check_call(['modprobe', 'mac80211_hwsim']) - # wait 5 seconds for fake devices to appear - timeout = 50 - while timeout > 0: - after_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) - if len(after_wlan) - len(before_wlan) >= 2: - break - timeout -= 1 - time.sleep(0.1) - else: - raise SystemError('timed out waiting for fake devices to appear') - - devs = list(after_wlan - before_wlan) - klass.dev_w_ap = devs[0] - klass.dev_w_client = devs[1] - - # don't let NM trample over our fake AP - with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: - f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) - @classmethod def shutdown_devices(klass): '''Remove test devices''' @@ -175,14 +160,10 @@ klass.dev_e_client = None klass.dev_e2_ap = None klass.dev_e2_client = None - klass.dev_w_ap = None - klass.dev_w_client = None subprocess.call(['ip', 'link', 'del', 'dev', 'mybr'], stderr=subprocess.PIPE) - subprocess.check_call(['rmmod', 'mac80211_hwsim']) - def setUp(self): '''Create test devices and workdir''' @@ -198,25 +179,6 @@ with open(self.entropy_file, 'wb') as f: f.write(b'012345678901234567890') - def setup_ap(self, hostapd_conf, ipv6_mode): - '''Set up simulated access point - - On self.dev_w_ap, run hostapd with given configuration. Setup dnsmasq - according to ipv6_mode, see start_dnsmasq(). - - This is torn down automatically at the end of the test. - ''' - - # give our AP an IP - subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_w_ap]) - if ipv6_mode is not None: - subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip6, 'dev', self.dev_w_ap]) - else: - subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip4, 'dev', self.dev_w_ap]) - - self.start_hostapd(hostapd_conf) - self.start_dnsmasq(ipv6_mode, self.dev_w_ap) - def setup_eth(self, ipv6_mode, start_dnsmasq=True): '''Set up simulated ethernet router @@ -237,6 +199,7 @@ subprocess.check_call(['ip', 'link', 'set', self.dev_e2_ap, 'up']) if start_dnsmasq: self.start_dnsmasq(ipv6_mode, self.dev_e_ap) + self.start_dnsmasq(ipv6_mode, self.dev_e2_ap) # # Internal implementation details @@ -269,19 +232,6 @@ assert timeout > 0, 'Timed out waiting for "%s":\n------------\n%s\n-------\n' % (string, log) - def start_hostapd(self, conf): - hostapd_conf = os.path.join(self.workdir, 'hostapd.conf') - with open(hostapd_conf, 'w') as f: - f.write('interface=%s\ndriver=nl80211\n' % self.dev_w_ap) - f.write(conf) - - log = os.path.join(self.workdir, 'hostapd.log') - p = subprocess.Popen(['hostapd', '-e', self.entropy_file, '-f', log, hostapd_conf], - stdout=subprocess.PIPE) - self.addCleanup(p.wait) - self.addCleanup(p.terminate) - self.poll_text(log, '' + self.dev_w_ap + ': AP-ENABLED', 500) - def start_dnsmasq(self, ipv6_mode, iface): '''Start dnsmasq. @@ -343,24 +293,18 @@ if 'bond' not in iface: self.assertIn('state UP', out) - if iface == self.dev_w_client: - out = subprocess.check_output(['iw', 'dev', iface, 'link'], - universal_newlines=True) - # self.assertIn('Connected to ' + self.mac_w_ap, out) - self.assertIn('SSID: fake net', out) - def generate_and_settle(self): '''Generate config, launch and settle NM and networkd''' # regenerate netplan config - out = subprocess.check_output(['netplan', 'apply'], universal_newlines=True) + out = subprocess.check_output(['netplan', 'apply'], stderr=subprocess.STDOUT, universal_newlines=True) if 'Run \'systemctl daemon-reload\' to reload units.' in out: self.fail('systemd units changed without reload') # start NM so that we can verify that it does not manage anything subprocess.check_call(['systemctl', 'start', '--no-block', 'NetworkManager.service']) # wait until networkd is done if self.is_active('systemd-networkd.service'): - if subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=20']) != 0: + if subprocess.call(['/lib/systemd/systemd-networkd-wait-online', '--quiet', '--timeout=30']) != 0: subprocess.call(['journalctl', '-b', '--no-pager', '-t', 'systemd-networkd']) st = subprocess.check_output(['networkctl'], stderr=subprocess.PIPE, universal_newlines=True) st_e = subprocess.check_output(['networkctl', 'status', self.dev_e_client], @@ -408,3 +352,110 @@ p = subprocess.Popen(['systemctl', 'is-active', unit], stdout=subprocess.PIPE) out = p.communicate()[0] return p.returncode == 0 or out.startswith(b'activating') + + +class IntegrationTestsWifi(IntegrationTestsBase): + '''Common functionality for network test cases + + setUp() creates two test wlan devices, one for a simulated access point + (self.dev_w_ap), the other for a simulated client device + (self.dev_w_client), and two test ethernet devices (self.dev_e_{ap,client} + and self.dev_e2_{ap,client}. + + Each test should call self.setup_ap() or self.setup_eth() with the desired + configuration. + ''' + @classmethod + def setUpClass(klass): + super().setUpClass() + # ensure we have this so that iw works + try: + subprocess.check_call(['modprobe', 'cfg80211']) + # set regulatory domain "EU", so that we can use 80211.a 5 GHz channels + out = subprocess.check_output(['iw', 'reg', 'get'], universal_newlines=True) + m = re.match(r'^(?:global\n)?country (\S+):', out) + assert m + klass.orig_country = m.group(1) + subprocess.check_call(['iw', 'reg', 'set', 'EU']) + except Exception: + raise unittest.SkipTest("cfg80211 (wireless) is unavailable, can't test") + + @classmethod + def tearDownClass(klass): + subprocess.check_call(['iw', 'reg', 'set', klass.orig_country]) + super().tearDownClass() + + @classmethod + def create_devices(klass): + '''Create Access Point and Client devices with mac80211_hwsim and veth''' + if os.path.exists('/sys/module/mac80211_hwsim'): + raise SystemError('mac80211_hwsim module already loaded') + super().create_devices() + # create virtual wlan devs + before_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) + subprocess.check_call(['modprobe', 'mac80211_hwsim']) + # wait 5 seconds for fake devices to appear + timeout = 50 + while timeout > 0: + after_wlan = set([c for c in os.listdir('/sys/class/net') if c.startswith('wlan')]) + if len(after_wlan) - len(before_wlan) >= 2: + break + timeout -= 1 + time.sleep(0.1) + else: + raise SystemError('timed out waiting for fake devices to appear') + + devs = list(after_wlan - before_wlan) + klass.dev_w_ap = devs[0] + klass.dev_w_client = devs[1] + + # don't let NM trample over our fake AP + with open('/run/NetworkManager/conf.d/test-blacklist.conf', 'w') as f: + f.write('[main]\nplugins=keyfile\n[keyfile]\nunmanaged-devices+=nptestsrv,%s\n' % klass.dev_w_ap) + + @classmethod + def shutdown_devices(klass): + '''Remove test devices''' + super().shutdown_devices() + klass.dev_w_ap = None + klass.dev_w_client = None + subprocess.check_call(['rmmod', 'mac80211_hwsim']) + + def start_hostapd(self, conf): + hostapd_conf = os.path.join(self.workdir, 'hostapd.conf') + with open(hostapd_conf, 'w') as f: + f.write('interface=%s\ndriver=nl80211\n' % self.dev_w_ap) + f.write(conf) + + log = os.path.join(self.workdir, 'hostapd.log') + p = subprocess.Popen(['hostapd', '-e', self.entropy_file, '-f', log, hostapd_conf], + stdout=subprocess.PIPE) + self.addCleanup(p.wait) + self.addCleanup(p.terminate) + self.poll_text(log, '' + self.dev_w_ap + ': AP-ENABLED', 500) + + def setup_ap(self, hostapd_conf, ipv6_mode): + '''Set up simulated access point + + On self.dev_w_ap, run hostapd with given configuration. Setup dnsmasq + according to ipv6_mode, see start_dnsmasq(). + + This is torn down automatically at the end of the test. + ''' + # give our AP an IP + subprocess.check_call(['ip', 'a', 'flush', 'dev', self.dev_w_ap]) + if ipv6_mode is not None: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip6, 'dev', self.dev_w_ap]) + else: + subprocess.check_call(['ip', 'a', 'add', self.dev_e_ap_ip4, 'dev', self.dev_w_ap]) + self.start_hostapd(hostapd_conf) + self.start_dnsmasq(ipv6_mode, self.dev_w_ap) + + def assert_iface_up(self, iface, expected_ip_a=None, unexpected_ip_a=None): + '''Assert that client interface is up''' + super().assert_iface_up(iface, expected_ip_a, unexpected_ip_a) + if iface == self.dev_w_client: + out = subprocess.check_output(['iw', 'dev', iface, 'link'], + universal_newlines=True) + # self.assertIn('Connected to ' + self.mac_w_ap, out) + self.assertIn('SSID: fake net', out) diff -Nru netplan.io-0.101/tests/integration/bonds.py netplan.io-0.102/tests/integration/bonds.py --- netplan.io-0.101/tests/integration/bonds.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/bonds.py 2021-03-16 11:38:25.000000000 +0000 @@ -354,6 +354,7 @@ match: name: %(ec)s macaddress: %(ec_mac)s + %(e2c)s: {} bonds: mybond: interfaces: [ethbn] @@ -428,7 +429,6 @@ def test_bond_arp_interval(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -458,7 +458,6 @@ def test_bond_arp_targets(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -488,7 +487,6 @@ def test_bond_arp_targets_many_lp1829264(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -520,7 +518,6 @@ def test_bond_arp_all_targets(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -552,7 +549,6 @@ def test_bond_arp_validate(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -651,7 +647,6 @@ def test_bond_arp_interval(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -681,7 +676,6 @@ def test_bond_arp_targets(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -711,7 +705,6 @@ def test_bond_arp_all_targets(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: diff -Nru netplan.io-0.101/tests/integration/bridges.py netplan.io-0.102/tests/integration/bridges.py --- netplan.io-0.101/tests/integration/bridges.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/bridges.py 2021-03-16 11:38:25.000000000 +0000 @@ -32,7 +32,6 @@ def test_eth_and_bridge(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: @@ -69,7 +68,6 @@ def test_bridge_path_cost(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -103,7 +101,6 @@ def test_bridge_ageing_time(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -136,7 +133,6 @@ def test_bridge_max_age(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -169,7 +165,6 @@ def test_bridge_hello_time(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -202,7 +197,6 @@ def test_bridge_forward_delay(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -235,7 +229,6 @@ def test_bridge_stp_false(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -275,7 +268,6 @@ def test_bridge_mac(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -284,6 +276,7 @@ match: name: %(ec)s macaddress: %(ec_mac)s + %(e2c)s: {} bridges: br0: interfaces: [ethbr] @@ -301,7 +294,6 @@ def test_bridge_anonymous(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -329,7 +321,6 @@ def test_bridge_isolated(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -349,7 +340,6 @@ def test_bridge_port_priority(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -393,7 +383,6 @@ def test_bridge_priority(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -426,7 +415,6 @@ def test_bridge_port_priority(self): self.setup_eth(None) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybr'], stderr=subprocess.DEVNULL) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s diff -Nru netplan.io-0.101/tests/integration/ethernets.py netplan.io-0.102/tests/integration/ethernets.py --- netplan.io-0.101/tests/integration/ethernets.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/ethernets.py 2021-03-16 11:38:25.000000000 +0000 @@ -32,7 +32,6 @@ def test_eth_mtu(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -55,7 +54,6 @@ def test_eth_mac(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -72,7 +70,7 @@ ['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]) @@ -81,7 +79,6 @@ Interface globbing was introduced as of NM 1.14+''' self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -152,7 +149,7 @@ elif resolved_in_use(): sys.stdout.write('[resolved] ') sys.stdout.flush() - out = subprocess.check_output(['systemd-resolve', '--status'], universal_newlines=True) + out = subprocess.check_output(['resolvectl', 'status'], universal_newlines=True) self.assertIn('DNS Servers: 172.1.2.3', out) self.assertIn('fakesuffix', out) else: @@ -175,7 +172,6 @@ addresses: ["172.16.7.2/30", "4321:AAAA::99/80"] dhcp4: yes ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) - self.start_dnsmasq(None, self.dev_e2_ap) self.generate_and_settle() if self.backend == 'NetworkManager': self.nm_online_full(self.dev_e2_client) diff -Nru netplan.io-0.101/tests/integration/ovs.py netplan.io-0.102/tests/integration/ovs.py --- netplan.io-0.101/tests/integration/ovs.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/ovs.py 2021-03-16 11:38:25.000000000 +0000 @@ -78,12 +78,13 @@ # If we have just OVS interfaces/ports networkd/networkctl will not be # aware that our network is ready. %(ec)s: {addresses: [10.10.10.20/24]} + %(e2c)s: {addresses: [10.10.10.30/24]} openvswitch: ports: - [patch0-1, patch1-0] bridges: ovs0: {interfaces: [patch0-1]} - ovs1: {interfaces: [patch1-0]}''' % {'ec': self.dev_e_client}) + ovs1: {interfaces: [patch1-0]}''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle() # Basic verification that the bridges/ports/interfaces are there in OVS out = subprocess.check_output(['ovs-vsctl', 'show']) @@ -119,12 +120,13 @@ f.write('''network: ethernets: %(ec)s: {addresses: [10.10.10.20/24]} + %(e2c)s: {addresses: [10.10.10.30/24]} openvswitch: ports: [[patch0-1, patch1-0]] bonds: bond0: {interfaces: [patch1-0, %(ec)s]} bridges: - ovs0: {interfaces: [patch0-1, bond0]}''' % {'ec': self.dev_e_client}) + ovs0: {interfaces: [patch0-1, bond0]}''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle() # Basic verification that the bridges/ports/interfaces are there in OVS out = subprocess.check_output(['ovs-vsctl', 'show']) @@ -137,6 +139,7 @@ f.write('''network: ethernets: %(ec)s: {addresses: [10.10.10.20/24]} + %(ec)s: {addresses: [10.10.10.30/24]} openvswitch: ports: [[patchx, patchy]] bonds: @@ -278,12 +281,12 @@ self.assertIn(b'---- mybond ----', out) self.assertIn(b'bond_mode: balance-slb', out) self.assertIn(b'lacp_status: off', out) - self.assertIn(b'slave %b: enabled' % self.dev_e_client.encode(), out) - self.assertIn(b'slave %b: enabled' % self.dev_e2_client.encode(), out) + self.assertRegex(out, br'(slave|member) %b: enabled' % self.dev_e_client.encode()) + self.assertRegex(out, br'(slave|member) %b: enabled' % self.dev_e2_client.encode()) self.assert_iface('ovsbr', ['inet 192.170.1.1/24']) def test_bridge_patch_ports(self): - self.setup_eth(None, False) + self.setup_eth(None) self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br0']) self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-br', 'br1']) self.addCleanup(subprocess.call, ['ovs-vsctl', '--if-exists', 'del-port', 'patch0-1']) @@ -374,11 +377,12 @@ nameservers: addresses: [10.5.32.99] search: [maas] + %(e2c)s: {} vlans: %(ec)s.21: id: 21 link: %(ec)s - mtu: 1500''' % {'ec': self.dev_e_client}) + mtu: 1500''' % {'ec': self.dev_e_client, 'e2c': self.dev_e2_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.101/tests/integration/regressions.py netplan.io-0.102/tests/integration/regressions.py --- netplan.io-0.101/tests/integration/regressions.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/regressions.py 2021-03-16 11:38:25.000000000 +0000 @@ -42,7 +42,6 @@ def test_lp1802322_bond_mac_rename(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'mybond'], stderr=subprocess.DEVNULL) with open(self.config, 'w') as f: f.write('''network: diff -Nru netplan.io-0.101/tests/integration/routing.py netplan.io-0.102/tests/integration/routing.py --- netplan.io-0.101/tests/integration/routing.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/routing.py 2021-03-16 11:38:25.000000000 +0000 @@ -35,7 +35,6 @@ The on-link option was introduced as of NM 1.12+ (for IPv4) The on-link option was introduced as of NM 1.18+ (for IPv6)''' self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -59,7 +58,6 @@ The from option was introduced as of NM 1.8+''' self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -82,7 +80,6 @@ The table option was introduced as of NM 1.10+''' self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) table_id = '255' # This is the 'local' FIB of /etc/iproute2/rt_tables with open(self.config, 'w') as f: f.write('''network: @@ -195,6 +192,41 @@ self.assertIn(b'mtu 777', # check mtu from static route subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + def test_per_route_congestion_window(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + gateway4: 192.168.5.1 + routes: + - to: 10.10.10.0/24 + via: 192.168.5.254 + congestion-window: 16''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle() + self.assertIn(b'initcwnd 16', # check initcwnd from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) + + def test_per_route_advertised_receive_window(self): + self.setup_eth(None) + with open(self.config, 'w') as f: + f.write('''network: + renderer: %(r)s + ethernets: + %(ec)s: + addresses: + - 192.168.5.99/24 + gateway4: 192.168.5.1 + routes: + - to: 10.10.10.0/24 + via: 192.168.5.254 + advertised-receive-window: 16''' % {'r': self.backend, 'ec': self.dev_e_client}) + self.generate_and_settle() + self.assertIn(b'initrwnd 16', # check initrwnd from static route + subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24'])) @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") @@ -228,7 +260,6 @@ @unittest.skip("networkd does not handle non-unicast routes correctly yet (Invalid argument)") def test_route_type_blackhole(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s @@ -248,7 +279,6 @@ def test_route_with_policy(self): self.setup_eth(None) - self.start_dnsmasq(None, self.dev_e2_ap) with open(self.config, 'w') as f: f.write('''network: renderer: %(r)s diff -Nru netplan.io-0.101/tests/integration/tunnels.py netplan.io-0.102/tests/integration/tunnels.py --- netplan.io-0.101/tests/integration/tunnels.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/tunnels.py 2021-03-16 11:38:25.000000000 +0000 @@ -67,6 +67,7 @@ mode: ipip local: 192.168.5.1 remote: 99.99.99.99 + ttl: 64 ''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client}) self.generate_and_settle() self.assert_iface('tun0', ['tun0@NONE', 'link.* 192.168.5.1 peer 99.99.99.99']) @@ -123,8 +124,8 @@ 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'transfer: \d+ B received, \d+ B sent') + self.assertRegex(out, r'latest handshake: (\d+ seconds? ago|Now)') + self.assertRegex(out, r'transfer: \d+.*B received, \d+.*B sent') self.assert_iface('wg0', ['inet 10.10.10.20/24']) # Verify client out = subprocess.check_output(['wg', 'show', 'wg1', 'private-key'], universal_newlines=True) @@ -138,7 +139,7 @@ self.assertIn("allowed ips: 0.0.0.0/0", out) self.assertIn("persistent keepalive: every 21 seconds", out) self.assertRegex(out, r'latest handshake: (\d+ seconds? ago|Now)') - self.assertRegex(out, r'transfer: \d+ B received, \d+ B sent') + self.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.101/tests/integration/wifi.py netplan.io-0.102/tests/integration/wifi.py --- netplan.io-0.101/tests/integration/wifi.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/integration/wifi.py 2021-03-16 11:38:25.000000000 +0000 @@ -24,7 +24,7 @@ import subprocess import unittest -from base import IntegrationTestsBase, test_backends +from base import IntegrationTestsWifi, test_backends class _CommonTests(): @@ -125,13 +125,13 @@ @unittest.skipIf("networkd" not in test_backends, "skipping as networkd backend tests are disabled") -class TestNetworkd(IntegrationTestsBase, _CommonTests): +class TestNetworkd(IntegrationTestsWifi, _CommonTests): backend = 'networkd' @unittest.skipIf("NetworkManager" not in test_backends, "skipping as NetworkManager backend tests are disabled") -class TestNetworkManager(IntegrationTestsBase, _CommonTests): +class TestNetworkManager(IntegrationTestsWifi, _CommonTests): backend = 'NetworkManager' def test_wifi_ap_open(self): diff -Nru netplan.io-0.101/tests/test_cli_get_set.py netplan.io-0.102/tests/test_cli_get_set.py --- netplan.io-0.101/tests/test_cli_get_set.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/test_cli_get_set.py 2021-03-16 11:38:25.000000000 +0000 @@ -69,6 +69,12 @@ with open(self.path, 'r') as f: self.assertIn('network:\n ethernets:\n eth0:\n dhcp4: \'yes\'', f.read()) + def test_set_global(self): + self._set([r'network={renderer: NetworkManager}']) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + self.assertIn('network:\n renderer: NetworkManager', f.read()) + def test_set_sequence(self): self._set(['ethernets.eth0.addresses=[1.2.3.4/24, \'5.6.7.8/24\']']) self.assertTrue(os.path.isfile(self.path)) @@ -132,14 +138,6 @@ 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: @@ -184,7 +182,7 @@ f.write('''network:\n version: 2\n renderer: NetworkManager ethernets: ens3: {dhcp4: yes, dhcp6: yes} - eth0: {addresses: [1.2.3.4]}''') + eth0: {addresses: [1.2.3.4/24]}''') self._set(['ethernets.eth0.addresses=NULL']) self._set(['ethernets.ens3.dhcp6=null']) self.assertTrue(os.path.isfile(self.path)) @@ -206,6 +204,17 @@ # The file should be deleted if this was the last/only key left self.assertFalse(os.path.isfile(self.path)) + def test_set_delete_file_with_version(self): + with open(self.path, 'w') as f: + f.write('''network: + version: 2 + ethernets: + ens3: {dhcp4: yes}''') + out = self._set(['network.ethernets.ens3=NULL']) + print(out, flush=True) + # The file should be deleted if only "network: {version: 2}" is left + self.assertFalse(os.path.isfile(self.path)) + def test_set_invalid_delete(self): with open(self.path, 'w') as f: f.write('''network:\n version: 2\n renderer: NetworkManager @@ -226,6 +235,55 @@ self.assertIsInstance(err, Exception) self.assertEquals('Invalid input: {\'network\': {\'ethernets\': {\'eth0\': {\'dhcp4:false\': None}}}}', str(err)) + def test_set_override_existing_file(self): + override = os.path.join(self.workdir.name, 'etc', 'netplan', 'some-file.yaml') + with open(override, 'w') as f: + f.write(r'network: {ethernets: {eth0: {dhcp4: true}, eth1: {dhcp6: false}}}') + self._set([r'ethernets.eth0.dhcp4=false']) + self.assertFalse(os.path.isfile(self.path)) + self.assertTrue(os.path.isfile(override)) + with open(override, 'r') as f: + out = f.read() + self.assertIn('network:\n ethernets:\n eth0:\n dhcp4: false', out) # new + self.assertIn('eth1:\n dhcp6: false', out) # old + + def test_set_override_existing_file_escaped_dot(self): + override = os.path.join(self.workdir.name, 'etc', 'netplan', 'some-file.yaml') + with open(override, 'w') as f: + f.write(r'network: {ethernets: {eth0.123: {dhcp4: true}}}') + self._set([r'ethernets.eth0\.123.dhcp4=false']) + self.assertFalse(os.path.isfile(self.path)) + self.assertTrue(os.path.isfile(override)) + with open(override, 'r') as f: + self.assertIn('network:\n ethernets:\n eth0.123:\n dhcp4: false', f.read()) + + def test_set_override_multiple_existing_files(self): + file1 = os.path.join(self.workdir.name, 'etc', 'netplan', 'eth0.yaml') + with open(file1, 'w') as f: + f.write(r'network: {ethernets: {eth0.1: {dhcp4: true}, eth0.2: {dhcp4: true}}}') + file2 = os.path.join(self.workdir.name, 'etc', 'netplan', 'eth1.yaml') + with open(file2, 'w') as f: + f.write(r'network: {ethernets: {eth1: {dhcp4: true}}}') + self._set([(r'network={renderer: NetworkManager, version: 2,' + r'ethernets:{' + r'eth1:{dhcp4: false},' + r'eth0.1:{dhcp4: false},' + r'eth0.2:{dhcp4: false}},' + r'bridges:{' + r'br99:{dhcp4: false}}}')]) + self.assertTrue(os.path.isfile(file1)) + with open(file1, 'r') as f: + self.assertIn('network:\n ethernets:\n eth0.1:\n dhcp4: false', f.read()) + self.assertTrue(os.path.isfile(file2)) + with open(file2, 'r') as f: + self.assertIn('network:\n ethernets:\n eth1:\n dhcp4: false', f.read()) + self.assertTrue(os.path.isfile(self.path)) + with open(self.path, 'r') as f: + out = f.read() + self.assertIn('network:\n bridges:\n br99:\n dhcp4: false', out) + self.assertIn(' version: 2', out) + self.assertIn(' renderer: NetworkManager', out) + class TestGet(unittest.TestCase): '''Test netplan get''' @@ -320,3 +378,9 @@ eth0: dhcp4: true version: 2\n''', out) + + def test_get_network(self): + with open(self.path, 'w') as f: + f.write('network:\n version: 2\n renderer: NetworkManager') + out = self._get(['network']) + self.assertEquals('renderer: NetworkManager\nversion: 2\n', out) diff -Nru netplan.io-0.101/tests/test_configmanager.py netplan.io-0.102/tests/test_configmanager.py --- netplan.io-0.101/tests/test_configmanager.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/test_configmanager.py 2021-03-16 11:38:25.000000000 +0000 @@ -118,6 +118,13 @@ addresses: - "2001:dead:beef::2/64" gateway6: "2001:dead:beef::1" + nm-devices: + fallback: + renderer: NetworkManager + networkmanager: + passthrough: + connection.id: some-nm-id + connection.uuid: some-uuid ''', file=fd) with open(os.path.join(self.workdir.name, "run/systemd/network/01-pretend.network"), 'w') as fd: print("pretend .network", file=fd) @@ -143,6 +150,7 @@ self.assertIn('ports', self.configmanager.openvswitch) self.assertEquals(2, self.configmanager.version) self.assertEquals('networkd', self.configmanager.renderer) + self.assertIn('fallback', self.configmanager.nm_devices) def test_parse_merging(self): self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile_merging.yaml")]) diff -Nru netplan.io-0.101/tests/test_nm_backend.py netplan.io-0.102/tests/test_nm_backend.py --- netplan.io-0.101/tests/test_nm_backend.py 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/tests/test_nm_backend.py 2021-03-16 11:38:25.000000000 +0000 @@ -0,0 +1,660 @@ +#!/usr/bin/python3 +# Blackbox tests of NetworkManager netplan backend. 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 shutil +import ctypes +import ctypes.util + +from generator.base import TestBase +from tests.test_utils import MockCmd + +rootdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +exe_cli = os.path.join(rootdir, 'src', 'netplan.script') +# Make sure we can import our development netplan. +os.environ.update({'PYTHONPATH': '.'}) + +lib = ctypes.CDLL(ctypes.util.find_library('netplan')) +lib.netplan_get_id_from_nm_filename.restype = ctypes.c_char_p + + +class TestNetworkManagerBackend(TestBase): + '''Test libnetplan functionality as used by NetworkManager backend''' + + def setUp(self): + super().setUp() + os.makedirs(self.confdir) + + def tearDown(self): + shutil.rmtree(self.workdir.name) + super().tearDown() + + def test_get_id_from_filename(self): + out = lib.netplan_get_id_from_nm_filename( + '/run/NetworkManager/system-connections/netplan-some-id.nmconnection'.encode(), None) + self.assertEqual(out, b'some-id') + + def test_get_id_from_filename_rootdir(self): + out = lib.netplan_get_id_from_nm_filename( + '/some/rootdir/run/NetworkManager/system-connections/netplan-some-id.nmconnection'.encode(), None) + self.assertEqual(out, b'some-id') + + def test_get_id_from_filename_wifi(self): + out = lib.netplan_get_id_from_nm_filename( + '/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID.nmconnection'.encode(), 'SOME-SSID'.encode()) + self.assertEqual(out, b'some-id') + + def test_get_id_from_filename_wifi_invalid_suffix(self): + out = lib.netplan_get_id_from_nm_filename( + '/run/NetworkManager/system-connections/netplan-some-id-SOME-SSID'.encode(), 'SOME-SSID'.encode()) + self.assertEqual(out, None) + + def test_get_id_from_filename_invalid_prefix(self): + out = lib.netplan_get_id_from_nm_filename('INVALID/netplan-some-id.nmconnection'.encode(), None) + self.assertEqual(out, None) + + def test_generate(self): + self.mock_netplan_cmd = MockCmd("netplan") + os.environ["TEST_NETPLAN_CMD"] = self.mock_netplan_cmd.path + self.assertTrue(lib.netplan_generate(self.workdir.name.encode())) + self.assertEquals(self.mock_netplan_cmd.calls(), [ + ["netplan", "generate", "--root-dir", self.workdir.name], + ]) + + def test_delete_connection(self): + os.environ["TEST_NETPLAN_CMD"] = exe_cli + FILENAME = os.path.join(self.confdir, 'some-filename.yaml') + with open(FILENAME, 'w') as f: + f.write('''network: + ethernets: + some-netplan-id: + dhcp4: true''') + self.assertTrue(os.path.isfile(FILENAME)) + # Parse all YAML and delete 'some-netplan-id' connection file + self.assertTrue(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) + self.assertFalse(os.path.isfile(FILENAME)) + + def test_delete_connection_id_not_found(self): + FILENAME = os.path.join(self.confdir, 'some-filename.yaml') + with open(FILENAME, 'w') as f: + f.write('''network: + ethernets: + some-netplan-id: + dhcp4: true''') + self.assertTrue(os.path.isfile(FILENAME)) + self.assertFalse(lib.netplan_delete_connection('unknown-id'.encode(), self.workdir.name.encode())) + self.assertTrue(os.path.isfile(FILENAME)) + + def test_delete_connection_two_in_file(self): + os.environ["TEST_NETPLAN_CMD"] = exe_cli + FILENAME = os.path.join(self.confdir, 'some-filename.yaml') + with open(FILENAME, 'w') as f: + f.write('''network: + ethernets: + some-netplan-id: + dhcp4: true + other-id: + dhcp6: true''') + self.assertTrue(os.path.isfile(FILENAME)) + self.assertTrue(lib.netplan_delete_connection('some-netplan-id'.encode(), self.workdir.name.encode())) + self.assertTrue(os.path.isfile(FILENAME)) + # Verify the file still exists and still contains the other connection + with open(FILENAME, 'r') as f: + self.assertEquals(f.read(), 'network:\n ethernets:\n other-id:\n dhcp6: true\n') + + def test_serialize_gsm(self): + self.maxDiff = None + UUID = 'a08c5805-7cf5-43f7-afb9-12cb30f6eca3' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection] +id=T-Mobile Funkadelic 2 +uuid=a08c5805-7cf5-43f7-afb9-12cb30f6eca3 +type=gsm + +[gsm] +apn=internet2.voicestream.com +device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 +home-only=true +network-id=254098 +password=parliament2 +pin=123456 +sim-id=89148000000060671234 +sim-operator-id=310260 +username=george.clinton.again + +[ipv4] +dns-search= +method=auto + +[ipv6] +addr-gen-mode=stable-privacy +dns-search= +method=auto +''') + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3'.encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + modems: + NM-{}: + renderer: NetworkManager + match: {{}} + apn: "internet2.voicestream.com" + device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" + network-id: "254098" + pin: "123456" + sim-id: "89148000000060671234" + sim-operator-id: "310260" + networkmanager: + uuid: {} + name: "T-Mobile Funkadelic 2" + passthrough: + gsm.home-only: "true" + gsm.password: "parliament2" + gsm.username: "george.clinton.again" + ipv4.dns-search: "" + ipv4.method: "auto" + ipv6.addr-gen-mode: "stable-privacy" + ipv6.dns-search: "" + ipv6.method: "auto" +'''.format(UUID, UUID)) + + def test_serialize_gsm_via_bluetooth(self): + self.maxDiff = None + UUID = 'a08c5805-7cf5-43f7-afb9-12cb30f6eca3' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection] +id=T-Mobile Funkadelic 2 +uuid=a08c5805-7cf5-43f7-afb9-12cb30f6eca3 +type=bluetooth + +[gsm] +apn=internet2.voicestream.com +device-id=da812de91eec16620b06cd0ca5cbc7ea25245222 +home-only=true +network-id=254098 +password=parliament2 +pin=123456 +sim-id=89148000000060671234 +sim-operator-id=310260 +username=george.clinton.again + +[ipv4] +dns-search= +method=auto + +[ipv6] +addr-gen-mode=stable-privacy +dns-search= +method=auto + +[proxy] +''') + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('NM-a08c5805-7cf5-43f7-afb9-12cb30f6eca3'.encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + nm-devices: + NM-{}: + renderer: NetworkManager + networkmanager: + uuid: {} + name: "T-Mobile Funkadelic 2" + passthrough: + connection.type: "bluetooth" + gsm.apn: "internet2.voicestream.com" + gsm.device-id: "da812de91eec16620b06cd0ca5cbc7ea25245222" + gsm.home-only: "true" + gsm.network-id: "254098" + gsm.password: "parliament2" + gsm.pin: "123456" + gsm.sim-id: "89148000000060671234" + gsm.sim-operator-id: "310260" + gsm.username: "george.clinton.again" + ipv4.dns-search: "" + ipv4.method: "auto" + ipv6.addr-gen-mode: "stable-privacy" + ipv6.dns-search: "" + ipv6.method: "auto" + proxy._: "" +'''.format(UUID, UUID)) + + def _template_serialize_keyfile(self, nd_type, nm_type, supported=True): + self.maxDiff = None + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('[connection]\ntype={}\nuuid={}'.format(nm_type, UUID)) + self.assertEqual(lib.netplan_clear_netdefs(), 0) + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + t = '\n passthrough:\n connection.type: "{}"'.format(nm_type) if not supported else '' + match = '\n match: {}' if nd_type in ['ethernets', 'modems', 'wifis'] else '' + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + {}: + NM-{}: + renderer: NetworkManager{} + networkmanager: + uuid: {}{} +'''.format(nd_type, UUID, match, UUID, t)) + + def test_serialize_keyfile_ethernet(self): + self._template_serialize_keyfile('ethernets', 'ethernet') + + def test_serialize_keyfile_type_modem_gsm(self): + self._template_serialize_keyfile('modems', 'gsm') + + def test_serialize_keyfile_type_modem_cdma(self): + self._template_serialize_keyfile('modems', 'cdma') + + def test_serialize_keyfile_type_bridge(self): + self._template_serialize_keyfile('bridges', 'bridge') + + def test_serialize_keyfile_type_bond(self): + self._template_serialize_keyfile('bonds', 'bond') + + def test_serialize_keyfile_type_vlan(self): + self._template_serialize_keyfile('vlans', 'vlan') + + def test_serialize_keyfile_type_tunnel(self): + self._template_serialize_keyfile('tunnels', 'ip-tunnel', False) + + def test_serialize_keyfile_type_wireguard(self): + self._template_serialize_keyfile('tunnels', 'wireguard', False) + + def test_serialize_keyfile_type_other(self): + self._template_serialize_keyfile('nm-devices', 'dummy', False) + + def test_serialize_keyfile_missing_uuid(self): + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('[connection]\ntype=ethernets') + self.assertFalse(lib.netplan_parse_keyfile(FILE.encode(), None)) + + def test_serialize_keyfile_missing_type(self): + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('[connection]\nuuid={}'.format(UUID)) + self.assertFalse(lib.netplan_parse_keyfile(FILE.encode(), None)) + + def test_serialize_keyfile_missing_file(self): + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + self.assertFalse(lib.netplan_parse_keyfile(FILE.encode(), None)) + + def test_serialize_keyfile_type_wifi(self): + self.maxDiff = None + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection] +type=wifi +uuid={} +permissions= +id=myid with spaces +interface-name=eth0 + +[wifi] +ssid=SOME-SSID +mode=infrastructure +hidden=true + +[ipv4] +method=auto +dns-search='''.format(UUID)) + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: + name: "eth0" + access-points: + "SOME-SSID": + hidden: true + mode: infrastructure + networkmanager: + uuid: {} + name: "myid with spaces" + passthrough: + connection.permissions: "" + ipv4.method: "auto" + ipv4.dns-search: "" + networkmanager: + uuid: {} + name: "myid with spaces" +'''.format(UUID, UUID, UUID)) + + def _template_serialize_keyfile_type_wifi(self, nd_mode, nm_mode): + self.maxDiff = None + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection] +type=wifi +uuid={} +id=myid with spaces + +[ipv4] +method=auto + +[wifi] +ssid=SOME-SSID +mode={}'''.format(UUID, nm_mode)) + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + wifi_mode = '' + if nm_mode != nd_mode: + wifi_mode = '\n wifi.mode: "{}"'.format(nm_mode) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + wifis: + NM-{}: + renderer: NetworkManager + match: {{}} + access-points: + "SOME-SSID": + mode: {} + networkmanager: + uuid: {} + name: "myid with spaces" + passthrough: + ipv4.method: "auto"{} + networkmanager: + uuid: {} + name: "myid with spaces" +'''.format(UUID, nd_mode, UUID, wifi_mode, UUID)) + + def test_serialize_keyfile_type_wifi_ap(self): + self._template_serialize_keyfile_type_wifi('ap', 'ap') + + def test_serialize_keyfile_type_wifi_adhoc(self): + self._template_serialize_keyfile_type_wifi('adhoc', 'adhoc') + + def test_serialize_keyfile_type_wifi_unknown(self): + self._template_serialize_keyfile_type_wifi('infrastructure', 'mesh') + + def test_serialize_keyfile_type_wifi_missing_ssid(self): + self.maxDiff = None + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection]\ntype=wifi\nuuid={}\nid=myid with spaces'''.format(UUID)) + self.assertFalse(lib.netplan_parse_keyfile(FILE.encode(), None)) + self.assertFalse(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + + def test_serialize_keyfile_wake_on_lan(self): + self.maxDiff = None + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection] +type=ethernet +uuid={} +id=myid with spaces + +[ethernet] +wake-on-lan=2 + +[ipv4] +method=auto'''.format(UUID)) + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + ethernets: + NM-{}: + renderer: NetworkManager + match: {{}} + wakeonlan: true + networkmanager: + uuid: {} + name: "myid with spaces" + passthrough: + ethernet.wake-on-lan: "2" + ipv4.method: "auto" +'''.format(UUID, UUID)) + + def test_serialize_keyfile_wake_on_lan_nm_default(self): + self.maxDiff = None + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection] +type=ethernet +uuid={} +id=myid with spaces + +[ethernet] + +[ipv4] +method=auto'''.format(UUID)) + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + ethernets: + NM-{}: + renderer: NetworkManager + match: {{}} + wakeonlan: true + networkmanager: + uuid: {} + name: "myid with spaces" + passthrough: + ethernet._: "" + ipv4.method: "auto" +'''.format(UUID, UUID)) + + def test_serialize_keyfile_modem_gsm(self): + self.maxDiff = None + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'tmp/some.keyfile') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection] +type=gsm +uuid={} +id=myid with spaces + +[ipv4] +method=auto + +[gsm] +auto-config=true'''.format(UUID)) + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + modems: + NM-{}: + renderer: NetworkManager + match: {{}} + auto-config: true + networkmanager: + uuid: {} + name: "myid with spaces" + passthrough: + ipv4.method: "auto" +'''.format(UUID, UUID)) + + def test_serialize_keyfile_existing_id(self): + self.maxDiff = None + UUID = '87749f1d-334f-40b2-98d4-55db58965f5f' + FILE = os.path.join(self.workdir.name, 'run/NetworkManager/system-connections/netplan-mybr.nmconnection') + os.makedirs(os.path.dirname(FILE)) + with open(FILE, 'w') as file: + file.write('''[connection] +type=bridge +uuid={} +id=renamed netplan bridge + +[ipv4] +method=auto'''.format(UUID)) + lib.netplan_parse_keyfile(FILE.encode(), None) + lib._write_netplan_conf('mybr'.encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)))) + with open(os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)), 'r') as f: + self.assertEqual(f.read(), '''network: + version: 2 + bridges: + mybr: + renderer: NetworkManager + networkmanager: + uuid: {} + name: "renamed netplan bridge" + passthrough: + ipv4.method: "auto" +'''.format(UUID)) + + def test_keyfile_yaml_wifi_hotspot(self): + self.maxDiff = None + UUID = 'ff9d6ebc-226d-4f82-a485-b7ff83b9607f' + FILE_KF = os.path.join(self.workdir.name, 'tmp/Hotspot.nmconnection') + CONTENT_KF = '''[connection] +id=Hotspot-1 +type=wifi +uuid={} +interface-name=wlan0 +#Netplan: passthrough setting +autoconnect=false +#Netplan: passthrough setting +permissions= + +[ipv4] +method=shared +#Netplan: passthrough setting +dns-search= + +[ipv6] +method=ignore +#Netplan: passthrough setting +addr-gen-mode=stable-privacy +#Netplan: passthrough setting +dns-search= + +[wifi] +ssid=my-hotspot +mode=ap +#Netplan: passthrough setting +mac-address-blacklist= + +[wifi-security] +#Netplan: passthrough setting +group=ccmp; +#Netplan: passthrough setting +key-mgmt=wpa-psk +#Netplan: passthrough setting +pairwise=ccmp; +#Netplan: passthrough setting +proto=rsn; +#Netplan: passthrough setting +psk=test1234 + +[proxy] +'''.format(UUID) + os.makedirs(os.path.dirname(FILE_KF)) + with open(FILE_KF, 'w') as file: + file.write(CONTENT_KF) + # Convert Keyfile to YAML and compare + lib.netplan_parse_keyfile(FILE_KF.encode(), None) + lib._write_netplan_conf('NM-{}'.format(UUID).encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + FILE_YAML = os.path.join(self.confdir, '90-NM-{}.yaml'.format(UUID)) + CONTENT_YAML = '''network: + version: 2 + wifis: + NM-ff9d6ebc-226d-4f82-a485-b7ff83b9607f: + renderer: NetworkManager + match: + name: "wlan0" + access-points: + "my-hotspot": + mode: ap + networkmanager: + uuid: ff9d6ebc-226d-4f82-a485-b7ff83b9607f + name: "Hotspot-1" + passthrough: + connection.autoconnect: "false" + connection.permissions: "" + ipv4.method: "shared" + ipv4.dns-search: "" + ipv6.method: "ignore" + ipv6.addr-gen-mode: "stable-privacy" + ipv6.dns-search: "" + wifi.mac-address-blacklist: "" + wifi-security.group: "ccmp;" + wifi-security.key-mgmt: "wpa-psk" + wifi-security.pairwise: "ccmp;" + wifi-security.proto: "rsn;" + wifi-security.psk: "test1234" + proxy._: "" + networkmanager: + uuid: {} + name: "Hotspot-1" +'''.format(UUID) + self.assertTrue(os.path.isfile(FILE_YAML)) + with open(FILE_YAML, 'r') as f: + self.assertEqual(f.read(), CONTENT_YAML) + + # Convert YAML back to Keyfile and compare to original KF + os.remove(FILE_YAML) + self.generate(CONTENT_YAML) + self.assert_nm({'NM-ff9d6ebc-226d-4f82-a485-b7ff83b9607f-my-hotspot': CONTENT_KF}) diff -Nru netplan.io-0.101/tests/test_serialize.py netplan.io-0.102/tests/test_serialize.py --- netplan.io-0.101/tests/test_serialize.py 1970-01-01 00:00:00.000000000 +0000 +++ netplan.io-0.102/tests/test_serialize.py 2021-03-16 11:38:25.000000000 +0000 @@ -0,0 +1,82 @@ +#!/usr/bin/python3 +# Blackbox tests of the netplan YAML serializer. These are run during +# "make check" and don't touch the system configuration at all. +# +# Copyright (C) 2021 Canonical, Ltd. +# Author: Lukas Märdian +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import shutil +import ctypes +import ctypes.util + +from generator.base import TestBase + +lib = ctypes.CDLL(ctypes.util.find_library('netplan')) + + +class TestNetplanSerialize(TestBase): + '''Test netplan's YAML serializer''' + + def setUp(self): + super().setUp() + os.makedirs(self.confdir) + + def tearDown(self): + shutil.rmtree(self.workdir.name) + super().tearDown() + + def _template_serialize_yaml(self, yaml_content, netdef_id='myid'): + FILENAME = os.path.join(self.confdir, 'some-filename.yaml') + NEWFILE = os.path.join(self.confdir, '10-netplan-{}.yaml'.format(netdef_id)) + with open(FILENAME, 'w') as f: + f.write(yaml_content) + # Parse YAML and and re-write the specified netdef ID into a new file + lib.netplan_parse_yaml(FILENAME.encode(), None) + lib._write_netplan_conf(netdef_id.encode(), self.workdir.name.encode()) + lib.netplan_clear_netdefs() + self.assertTrue(os.path.isfile(NEWFILE)) + with open(FILENAME, 'r') as f: + with open(NEWFILE, 'r') as new: + self.assertEquals(f.read(), new.read()) + + def test_serialize_yaml_basic(self): + self._template_serialize_yaml('''network: + version: 2 + ethernets: + some-netplan-id: + renderer: networkd + match: + name: "eth42" +''', 'some-netplan-id') + + def test_serialize_yaml_wifi_ap(self): + self._template_serialize_yaml('''network: + version: 2 + wifis: + myid: + renderer: NetworkManager + match: + name: "eth42" + access-points: + "SOME-SSID": + hidden: true + mode: infrastructure + networkmanager: + uuid: some-uuid + name: "Some NM name with spaces" + passthrough: + wifi.mode: "mesh" +''', 'myid') diff -Nru netplan.io-0.101/tests/test_utils.py netplan.io-0.102/tests/test_utils.py --- netplan.io-0.101/tests/test_utils.py 2020-12-09 11:32:25.000000000 +0000 +++ netplan.io-0.102/tests/test_utils.py 2021-03-16 11:38:25.000000000 +0000 @@ -19,15 +19,72 @@ import unittest import tempfile import glob - -from unittest.mock import patch +import netifaces import netplan.cli.utils as utils +from unittest.mock import patch DEVICES = ['eth0', 'eth1', 'ens3', 'ens4', 'br0'] +# Consider switching to something more standard, like MockProc +class MockCmd: + """MockCmd will mock a given command name and capture all calls to it""" + + def __init__(self, name): + self._tmp = tempfile.TemporaryDirectory() + self.name = name + self.path = os.path.join(self._tmp.name, name) + self.call_log = os.path.join(self._tmp.name, "call.log") + with open(self.path, "w") as fp: + fp.write("""#!/bin/bash +printf "%%s" "$(basename "$0")" >> %(log)s +printf '\\0' >> %(log)s + +for arg in "$@"; do + printf "%%s" "$arg" >> %(log)s + printf '\\0' >> %(log)s +done + +printf '\\0' >> %(log)s +""" % {'log': self.call_log}) + os.chmod(self.path, 0o755) + + def calls(self): + """ + calls() returns the calls to the given mock command in the form of + [ ["cmd", "call1-arg1"], ["cmd", "call2-arg1"], ... ] + """ + with open(self.call_log) as fp: + b = fp.read() + calls = [] + for raw_call in b.rstrip("\0\0").split("\0\0"): + call = raw_call.rstrip("\0") + calls.append(call.split("\0")) + return calls + + def set_output(self, output): + with open(self.path, "a") as fp: + fp.write("cat << EOF\n%s\nEOF" % output) + + def set_timeout(self, timeout_dsec=10): + with open(self.path, "a") as fp: + fp.write(""" +if [[ "$*" == *try* ]] +then + ACTIVE=1 + trap 'ACTIVE=0' SIGUSR1 + trap 'ACTIVE=0' SIGINT + while (( $ACTIVE > 0 )) && (( $ACTIVE <= {} )) + do + ACTIVE=$(($ACTIVE+1)) + sleep 0.1 + done +fi +""".format(timeout_dsec)) + + class TestUtils(unittest.TestCase): def setUp(self): @@ -96,3 +153,42 @@ match = {'name': 'ens?', 'driver': 'f*'} iface = utils.find_matching_iface(DEVICES, match) self.assertEqual(iface, 'ens4') + + @patch('netifaces.ifaddresses') + def test_interface_macaddress(self, ifaddr): + ifaddr.side_effect = lambda _: {netifaces.AF_LINK: [{'addr': '00:01:02:03:04:05'}]} + self.assertEqual(utils.get_interface_macaddress('eth42'), '00:01:02:03:04:05') + + @patch('netifaces.ifaddresses') + def test_interface_macaddress_empty(self, ifaddr): + ifaddr.side_effect = lambda _: {} + self.assertEqual(utils.get_interface_macaddress('eth42'), '') + + def test_netplan_get_filename_by_id(self): + file_a = os.path.join(self.workdir.name, 'etc/netplan/a.yaml') + file_b = os.path.join(self.workdir.name, 'etc/netplan/b.yaml') + with open(file_a, 'w') as f: + f.write('network:\n ethernets:\n id_a:\n dhcp4: true') + with open(file_b, 'w') as f: + f.write('network:\n ethernets:\n id_b:\n dhcp4: true\n id_a:\n dhcp4: true') + # netdef:b can only be found in b.yaml + basename = os.path.basename(utils.netplan_get_filename_by_id('id_b', self.workdir.name)) + self.assertEqual(basename, 'b.yaml') + # netdef:a is defined in a.yaml, overriden by b.yaml + basename = os.path.basename(utils.netplan_get_filename_by_id('id_a', self.workdir.name)) + self.assertEqual(basename, 'b.yaml') + + def test_netplan_get_filename_by_id_no_files(self): + self.assertIsNone(utils.netplan_get_filename_by_id('some-id', self.workdir.name)) + + def test_netplan_get_filename_by_id_invalid(self): + file = os.path.join(self.workdir.name, 'etc/netplan/a.yaml') + with open(file, 'w') as f: + f.write('''network: + tunnels: + id_a: + mode: sit + local: 0.0.0.0 + remote: 0.0.0.0 + key: 0.0.0.0''') + self.assertIsNone(utils.netplan_get_filename_by_id('some-id', self.workdir.name))