Merge ~schopin/netplan/+git/ubuntu:hirsute-sru into ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu/focal

Proposed by Simon Chopin
Status: Rejected
Rejected by: Lukas Märdian
Proposed branch: ~schopin/netplan/+git/ubuntu:hirsute-sru
Merge into: ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu/focal
Diff against target: 1161 lines (+1007/-12) (has conflicts)
7 files modified
debian/changelog (+34/-0)
debian/control (+4/-0)
debian/gbp.conf (+4/-0)
debian/patches/0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch (+583/-0)
debian/patches/0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch (+368/-0)
debian/patches/series (+2/-0)
debian/tests/control (+12/-12)
Conflict in debian/changelog
Conflict in debian/control
Conflict in debian/gbp.conf
Reviewer Review Type Date Requested Status
Lukas Märdian Pending
Review via email: mp+409761@code.launchpad.net

This proposal has been superseded by a proposal from 2021-10-06.

To post a comment you must log in.
Revision history for this message
Lukas Märdian (slyon) :

Unmerged commits

1e04bd7... by Simon Chopin

Update changelog to document the 2 previous commits

9b7db72... by Lukas Märdian

Add d/p/0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch (LP: #1942930)

b979afb... by Lukas Märdian

Add d/p/0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch

Allow to pass a state to netplan apply/try so it can cleanup unused
virtual network interfaces after itself. Make use of this functionality
inside the DBus Config.Try()/Apply() API and the 'netplan try' CLI.
(LP: #1943120)

e533bb6... by Lukas Märdian

releasing 0.103-0ubuntu5~21.04.1

2e5cce9... by Lukas Märdian

releasing netplan.io 0.103-0ubuntu5~21.04.1

70cfab1... by Lukas Märdian

update changelog

2be9304... by Lukas Märdian

d/p/0001-parse-nm-fix-32bit-format-string.patch drop unrelated change

71c6a80... by Lukas Märdian

Merge tag 'ubuntu/0.103-0ubuntu5' into ubuntu/hirsute

netplan.io Debian release 0.103-0ubuntu5

366e359... by Lukas Märdian

update changelog

d25a8ec... by Lukas Märdian

refresh patches

Update scan failed

At least one of the branches involved have failed to scan. You can manually schedule a rescan if required.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index 0a1aeff..7e22b8e 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,12 +1,33 @@
6+<<<<<<< debian/changelog
7 netplan.io (0.103-0ubuntu5~20.04.1) focal; urgency=medium
8
9 * Backport netplan.io 0.103-0ubuntu5 to 20.04 (LP: #1938920)
10+=======
11+netplan.io (0.103-0ubuntu5~21.04.2) hirsute; urgency=medium
12+
13+ * Backport patches from impish:
14+ + Add d/p/0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch:
15+ Fix unset of a devtype subtree, e.g. "netplan set network.ethernets=null".
16+ (LP: #1942930)
17+ + Add d/p/0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch:
18+ Allow to pass a state to netplan apply/try so it can cleanup unused
19+ virtual network interfaces after itself. Make use of this functionality
20+ inside the DBus Config.Try()/Apply() API and the 'netplan try' CLI.
21+ (LP: #1943120)
22+
23+ -- Simon Chopin <simon.chopin@canonical.com> Wed, 06 Oct 2021 12:57:35 +0200
24+
25+netplan.io (0.103-0ubuntu5~21.04.1) hirsute; urgency=medium
26+
27+ * Backport netplan.io 0.103-0ubuntu5 to 21.04 (LP: #1938920)
28+>>>>>>> debian/changelog
29 - Add YAML generator and Keyfile parser for NetworkManager YAML backend
30 - Add activation-mode parameter
31 - Add io.netplan.Netplan.Generate() DBus method
32 - Changed the way of how unmanaged-devices are handled by NetworkManager
33 * Add d/p/0003-Mute-gateway4-6-deprecation-warnings.patch:
34 - Do not show a deprecation warning on the backported version
35+<<<<<<< debian/changelog
36 * Update systemd dependency to >= 245.4-4ubuntu3.8 for activation-mode
37 * Update debian/gbp.conf
38 * Use debhelper-compat 12 in debian/control
39@@ -23,6 +44,19 @@ netplan.io (0.102-0ubuntu1~20.04.2) focal; urgency=medium
40 * Keep riscv64 build-time tests disabled
41 * Add d/p/0002-tests-tunnels-improve-flaky-wireguard-test-with-wait.patch
42 * Fix regression (LP: #1922898), by avoiding to break the ABI
43+=======
44+ * Update systemd dependency to >= 247.3-3ubuntu3.2 for activation-mode
45+ * Update debian/gbp.conf
46+ * Use debhelper-compat 12 in debian/control
47+ * Keep skipping many armhf/LXD tests
48+ * Skip flaky 'regressions' test on armhf/LXD containers, too
49+
50+ -- Lukas Märdian <slyon@ubuntu.com> Tue, 07 Sep 2021 10:50:30 +0200
51+
52+netplan.io (0.102-0ubuntu3) hirsute; urgency=medium
53+
54+ * Fix regression (LP: #1922898) for good, by avoiding to break the ABI
55+>>>>>>> debian/changelog
56 This reverts the "Added ttl option for tunnels" feature
57
58 -- Lukas Märdian <slyon@ubuntu.com> Mon, 19 Apr 2021 15:08:37 +0200
59diff --git a/debian/control b/debian/control
60index 148376d..33f44e7 100644
61--- a/debian/control
62+++ b/debian/control
63@@ -43,7 +43,11 @@ Depends:
64 python3,
65 python3-yaml,
66 python3-netifaces,
67+<<<<<<< debian/control
68 systemd (>= 245.4-4ubuntu3.8),
69+=======
70+ systemd (>= 247.3-3ubuntu3.2),
71+>>>>>>> debian/control
72 Suggests:
73 network-manager | wpasupplicant,
74 openvswitch-switch [!riscv64],
75diff --git a/debian/gbp.conf b/debian/gbp.conf
76index 99901e9..b21147c 100644
77--- a/debian/gbp.conf
78+++ b/debian/gbp.conf
79@@ -1,5 +1,9 @@
80 [DEFAULT]
81+<<<<<<< debian/gbp.conf
82 debian-branch=ubuntu/focal
83+=======
84+debian-branch=ubuntu/hirsute
85+>>>>>>> debian/gbp.conf
86 debian-tag=ubuntu/%(version)s
87 upstream-branch=upstream/latest
88 upstream-vcs-tag=%(version)s
89diff --git a/debian/patches/0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch b/debian/patches/0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch
90new file mode 100644
91index 0000000..a51ee6c
92--- /dev/null
93+++ b/debian/patches/0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch
94@@ -0,0 +1,583 @@
95+From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
96+Date: Mon, 27 Sep 2021 16:40:45 +0200
97+Subject: Implement YAML state tracking and use it in the DBus API and
98+ netplan-try (LP: #1943120) (FR-1745) (#231)
99+
100+Allow to pass an optional --state parameter to netplan try/apply describing a directory that contains a netplan configuration tree/state (i.e. /{etc,run,lib}/netplan/*.yaml).
101+Netplan will make use of this "old state" to calculate the delta of dropped interface definitions, like bridges/bonds/vlans/tunnels that have been configured before but are not part of the current YAML configuration anymore. It will then try to delete those virtual interfaces (via ip link del dev IFACE) if they still exist.
102+
103+The same functionality is used to roll back a netplan try command that failed or was rejected.
104+
105+Generally, the state needs to be provided manually. The DBus API (using io.netplan.Netplan.Config.Try/Apply) is an exception, as the previous state can be backed up automatically in this case.
106+
107+COMMITS:
108+* cli:apply:configmanager: clear_virtual_links during apply
109+* tests:scenarios: check virtual interface cleanup
110+* doc:apply: update manpage
111+* cli:try: use clear_virtual_links from Apply()
112+* dbus: make use of new YAML state tracking
113+* dbus: properly create and clean the backup state dir
114+* cli:apply:clear_virtual_links: make devices a named parameter
115+---
116+ doc/netplan-apply.md | 18 ++++++-----
117+ netplan/cli/commands/apply.py | 40 +++++++++++++++++++++++-
118+ netplan/cli/commands/try_command.py | 28 ++++++++---------
119+ netplan/configmanager.py | 10 ++++++
120+ src/dbus.c | 62 ++++++++++++++++++++++++-------------
121+ tests/dbus/test_dbus.py | 24 ++++++++++----
122+ tests/integration/base.py | 7 +++--
123+ tests/integration/scenarios.py | 27 ++++++++++++++++
124+ tests/test_cli_units.py | 40 ++++++++++++++++++++++++
125+ tests/test_configmanager.py | 6 ++++
126+ 10 files changed, 211 insertions(+), 51 deletions(-)
127+
128+diff --git a/doc/netplan-apply.md b/doc/netplan-apply.md
129+index 153acb1..7811f04 100644
130+--- a/doc/netplan-apply.md
131++++ b/doc/netplan-apply.md
132+@@ -47,13 +47,17 @@ see **netplan**(5).
133+
134+ # KNOWN ISSUES
135+
136+-**netplan apply** will not remove virtual devices such as bridges
137+-and bonds that have been created, even if they are no longer described
138+-in the netplan configuration.
139+-
140+-This can be resolved by manually removing the virtual device (for
141+-example ``ip link delete dev bond0``) and then running **netplan
142+-apply**, or by rebooting.
143++**netplan apply** will not remove virtual devices such as bridges and bonds
144++that have been created, even if they are no longer described in the netplan
145++configuration. That is due to the fact that netplan operates statelessly and
146++is not aware of the previously defined virtal devices.
147++
148++This can be resolved by manually removing the virtual device (for example
149++``ip link delete dev bond0``) and then running **netplan apply**, by rebooting,
150++or by creating a temporary backup of the YAML state in ``/etc/netplan``
151++before modifying the configuration and passing this state to netplan (e.g.
152++``mkdir -p /tmp/netplan_state_backup/etc && cp -r /etc/netplan /tmp/netplan_state_backup/etc/``
153++then running **netplan apply --state /tmp/netplan_state_backup**)
154+
155+
156+ # SEE ALSO
157+diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py
158+index c3f5caa..477bc2f 100644
159+--- a/netplan/cli/commands/apply.py
160++++ b/netplan/cli/commands/apply.py
161+@@ -48,14 +48,18 @@ class NetplanApply(utils.NetplanCommand):
162+ help='Only apply SR-IOV related configuration and exit')
163+ self.parser.add_argument('--only-ovs-cleanup', action='store_true',
164+ help='Only clean up old OpenVSwitch interfaces and exit')
165++ self.parser.add_argument('--state',
166++ help='Directory containing previous YAML configuration')
167+
168+ self.func = self.command_apply
169+
170+ self.parse_args()
171+ self.run_command()
172+
173+- def command_apply(self, run_generate=True, sync=False, exit_on_error=True): # pragma: nocover (covered in autopkgtest)
174++ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state_dir=None): # pragma: nocover
175+ config_manager = ConfigManager()
176++ if state_dir:
177++ self.state = state_dir
178+
179+ # For certain use-cases, we might want to only apply specific configuration.
180+ # If we only need SR-IOV configuration, do that and exit early.
181+@@ -190,6 +194,14 @@ class NetplanApply(utils.NetplanCommand):
182+ # for now, only applies to non-virtual (real) devices.
183+ config_manager.parse()
184+ changes = NetplanApply.process_link_changes(devices, config_manager)
185++ # delete virtual interfaces that have been defined in a previous state
186++ # but are not configured anymore in the current YAML
187++ if self.state:
188++ cm = ConfigManager(self.state)
189++ cm.parse() # get previous configuration state
190++ prev_links = cm.virtual_interfaces.keys()
191++ curr_links = config_manager.virtual_interfaces.keys()
192++ NetplanApply.clear_virtual_links(prev_links, curr_links, devices)
193+
194+ # if the interface is up, we can still apply some .link file changes
195+ # but we cannot apply the interface rename via udev, as it won't touch
196+@@ -259,6 +271,32 @@ class NetplanApply(utils.NetplanCommand):
197+
198+ return False
199+
200++ @staticmethod
201++ def clear_virtual_links(prev_links, curr_links, devices=[]):
202++ """
203++ Calculate the delta of virtual links. And remove the links that were
204++ dropped from the YAML config, if they were not dropped by the backend
205++ already.
206++ We can make use of the netplan netdef ids, as those equal the interface
207++ name for virtual links.
208++ """
209++ if not devices:
210++ logging.warning('Cannot clear virtual links: no network interfaces provided.')
211++ return []
212++
213++ dropped_interfaces = list(set(prev_links) - set(curr_links))
214++ # some interfaces might have been cleaned up already, e.g. by the
215++ # NetworkManager backend
216++ interfaces_to_clear = list(set(dropped_interfaces).intersection(devices))
217++ for link in interfaces_to_clear:
218++ try:
219++ cmd = ['ip', 'link', 'delete', 'dev', link]
220++ subprocess.check_call(cmd)
221++ except subprocess.CalledProcessError:
222++ logging.warn('Could not delete interface {}'.format(link))
223++
224++ return dropped_interfaces
225++
226+ @staticmethod
227+ def process_link_changes(interfaces, config_manager): # pragma: nocover (covered in autopkgtest)
228+ """
229+diff --git a/netplan/cli/commands/try_command.py b/netplan/cli/commands/try_command.py
230+index 198992f..5534596 100644
231+--- a/netplan/cli/commands/try_command.py
232++++ b/netplan/cli/commands/try_command.py
233+@@ -19,10 +19,11 @@
234+
235+ import os
236+ import time
237++import shutil
238+ import signal
239+ import sys
240++import tempfile
241+ import logging
242+-import subprocess
243+
244+ from netplan.configmanager import ConfigManager
245+ import netplan.cli.utils as utils
246+@@ -59,6 +60,8 @@ class NetplanTry(utils.NetplanCommand):
247+ self.parser.add_argument('--timeout',
248+ type=int, default=DEFAULT_INPUT_TIMEOUT,
249+ help="Maximum number of seconds to wait for the user's confirmation")
250++ self.parser.add_argument('--state',
251++ help='Directory containing previous YAML configuration')
252+
253+ self.func = self.command_try
254+
255+@@ -81,7 +84,7 @@ class NetplanTry(utils.NetplanCommand):
256+ self.backup()
257+ self.setup()
258+
259+- NetplanApply().command_apply(run_generate=True, sync=True, exit_on_error=False)
260++ NetplanApply().command_apply(run_generate=True, sync=True, exit_on_error=False, state_dir=self.state)
261+
262+ self.t.get_confirmation_input(timeout=self.timeout)
263+ except netplan.terminal.InputRejected:
264+@@ -114,19 +117,16 @@ class NetplanTry(utils.NetplanCommand):
265+ self.configuration_changed = True
266+
267+ def revert(self): # pragma: nocover (requires user input)
268++ # backup the state we just tried to apply
269++ tempdir = tempfile.mkdtemp()
270++ confdir = os.path.join(tempdir, 'etc', 'netplan')
271++ os.makedirs(confdir)
272++ shutil.copytree('/etc/netplan', confdir, dirs_exist_ok=True)
273++ # restore previous state
274+ self.config_manager.revert()
275+- NetplanApply().command_apply(run_generate=False, sync=True, exit_on_error=False)
276+- for ifname in self.new_interfaces:
277+- if ifname not in self.config_manager.bonds and \
278+- ifname not in self.config_manager.bridges and \
279+- ifname not in self.config_manager.vlans:
280+- logging.debug("{} will not be removed: not a virtual interface".format(ifname))
281+- continue
282+- try:
283+- cmd = ['ip', 'link', 'del', ifname]
284+- subprocess.check_call(cmd)
285+- except subprocess.CalledProcessError:
286+- logging.warn("Could not revert (remove) new interface '{}'".format(ifname))
287++ NetplanApply().command_apply(run_generate=False, sync=True, exit_on_error=False, state_dir=tempdir)
288++ # clear the backup
289++ shutil.rmtree(tempdir)
290+
291+ def cleanup(self): # pragma: nocover (requires user input)
292+ self.config_manager.cleanup()
293+diff --git a/netplan/configmanager.py b/netplan/configmanager.py
294+index 9278d04..133dc5b 100644
295+--- a/netplan/configmanager.py
296++++ b/netplan/configmanager.py
297+@@ -62,6 +62,16 @@ class ConfigManager(object):
298+ interfaces.update(self.wifis)
299+ return interfaces
300+
301++ @property
302++ def virtual_interfaces(self):
303++ interfaces = {}
304++ # what about ovs_ports?
305++ interfaces.update(self.bridges)
306++ interfaces.update(self.bonds)
307++ interfaces.update(self.tunnels)
308++ interfaces.update(self.vlans)
309++ return interfaces
310++
311+ @property
312+ def ovs_ports(self):
313+ return self.network['ovs_ports']
314+diff --git a/src/dbus.c b/src/dbus.c
315+index f0aa53a..a486b54 100644
316+--- a/src/dbus.c
317++++ b/src/dbus.c
318+@@ -199,6 +199,30 @@ _clear_tmp_state(const char *config_id, NetplanData *d)
319+ return TRUE;
320+ }
321+
322++static int
323++_backup_global_state(sd_bus_error *ret_error)
324++{
325++ int r = 0;
326++ g_autofree gchar *path = NULL;
327++ path = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG);
328++ /* Create {etc,run,lib} subdirs with owner r/w permissions */
329++ char *subdir = NULL;
330++ for (int i = 0; i < 3; i++) {
331++ subdir = g_strdup_printf("%s/%s/netplan", path, NETPLAN_SUBDIRS[i]);
332++ r = g_mkdir_with_parents(subdir, 0700);
333++ if (r < 0)
334++ // LCOV_EXCL_START
335++ return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED,
336++ "Failed to create '%s': %s\n", subdir, strerror(errno));
337++ // LCOV_EXCL_STOP
338++ g_free(subdir);
339++ }
340++
341++ /* Copy main *.yaml files from /{etc,run,lib}/netplan/ to GLOBAL backup dir */
342++ _copy_yaml_state(NETPLAN_ROOT, path, ret_error);
343++ return 0;
344++}
345++
346+ /**
347+ * io.netplan.Netplan methods
348+ */
349+@@ -209,6 +233,7 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
350+ g_autoptr(GError) err = NULL;
351+ g_autofree gchar *stdout = NULL;
352+ g_autofree gchar *stderr = NULL;
353++ g_autofree gchar *state = NULL;
354+ gint exit_status = 0;
355+ NetplanData *d = userdata;
356+
357+@@ -216,8 +241,9 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
358+ * Otherwise execute 'netplan apply' directly. */
359+ if (d->try_pid > 0)
360+ return _try_accept(TRUE, m, userdata, ret_error);
361+-
362+- gchar *argv[] = {SBINDIR "/" "netplan", "apply", NULL};
363++ if (d->config_id)
364++ state = g_strdup_printf("--state=%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG);
365++ gchar *argv[] = {SBINDIR "/" "netplan", "apply", state, NULL};
366+
367+ // for tests only: allow changing what netplan to run
368+ if (getenv("DBUS_TEST_NETPLAN_CMD") != 0)
369+@@ -421,6 +447,7 @@ method_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
370+ {
371+ g_autoptr(GError) err = NULL;
372+ g_autofree gchar *timeout = NULL;
373++ g_autofree gchar *state = NULL;
374+ gint child_stdin = -1; /* child process needs an input to function correctly */
375+ guint seconds = 0;
376+ int r = -1;
377+@@ -430,7 +457,9 @@ method_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
378+ return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot extract timeout_seconds"); // LCOV_EXCL_LINE
379+ if (seconds > 0)
380+ timeout = g_strdup_printf("--timeout=%u", seconds);
381+- gchar *argv[] = {SBINDIR "/" "netplan", "try", timeout, NULL};
382++ if (d->config_id)
383++ state = g_strdup_printf("--state=%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG);
384++ gchar *argv[] = {SBINDIR "/" "netplan", "try", timeout, state, NULL};
385+
386+ // for tests only: allow changing what netplan to run
387+ if (getenv("DBUS_TEST_NETPLAN_CMD") != 0)
388+@@ -482,6 +511,10 @@ method_config_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
389+ d->config_dirty = g_strdup(d->config_id);
390+
391+ if (d->try_pid < 0) {
392++ r = _backup_global_state(ret_error);
393++ if (r < 0)
394++ return r; // LCOV_EXCL_LINE
395++
396+ /* Delete GLOBAL state */
397+ unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml");
398+ /* Copy current config state to GLOBAL */
399+@@ -491,6 +524,8 @@ method_config_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
400+ }
401+
402+ r = method_apply(m, d, ret_error);
403++ /* Clear GLOBAL backup and config state */
404++ _clear_tmp_state(NETPLAN_GLOBAL_CONFIG, d);
405+ _clear_tmp_state(d->config_id, d);
406+
407+ /* unlock current config ID and handler ID */
408+@@ -535,7 +570,6 @@ static int
409+ method_config_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
410+ {
411+ NetplanData *d = userdata;
412+- g_autofree gchar *path = NULL;
413+ g_autofree gchar *state_dir = NULL;
414+ const char *config_id = sd_bus_message_get_path(m) + 27;
415+ if (d->try_pid > 0)
416+@@ -551,23 +585,9 @@ method_config_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
417+ d->try_pid = G_MAXINT;
418+ d->config_id = config_id;
419+
420+- /* Backup GLOBAL state */
421+- path = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG);
422+- /* Create {etc,run,lib} subdirs with owner r/w permissions */
423+- char *subdir = NULL;
424+- for (int i = 0; i < 3; i++) {
425+- subdir = g_strdup_printf("%s/%s/netplan", path, NETPLAN_SUBDIRS[i]);
426+- r = g_mkdir_with_parents(subdir, 0700);
427+- if (r < 0)
428+- // LCOV_EXCL_START
429+- return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED,
430+- "Failed to create '%s': %s\n", subdir, strerror(errno));
431+- // LCOV_EXCL_STOP
432+- g_free(subdir);
433+- }
434+-
435+- /* Copy main *.yaml files from /{etc,run,lib}/netplan/ to GLOBAL backup dir */
436+- _copy_yaml_state(NETPLAN_ROOT, path, ret_error);
437++ r = _backup_global_state(ret_error);
438++ if (r < 0)
439++ return r; // LCOV_EXCL_LINE
440+
441+ /* Clear main *.yaml files */
442+ unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml");
443+diff --git a/tests/dbus/test_dbus.py b/tests/dbus/test_dbus.py
444+index 87abf22..5c2f5b7 100644
445+--- a/tests/dbus/test_dbus.py
446++++ b/tests/dbus/test_dbus.py
447+@@ -361,6 +361,7 @@ class TestNetplanDBus(unittest.TestCase):
448+ def test_netplan_dbus_config_cancel(self):
449+ cid = self._new_config_object()
450+ tmpdir = '/tmp/netplan-config-{}'.format(cid)
451++ backup = '/tmp/netplan-config-BACKUP'
452+
453+ # Verify .Config.Cancel() teardown of the config object and state dirs
454+ BUSCTL_NETPLAN_CMD = [
455+@@ -380,9 +381,14 @@ class TestNetplanDBus(unittest.TestCase):
456+ err = self._check_dbus_error(BUSCTL_NETPLAN_CMD)
457+ self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err)
458+
459++ # Verify the backup and config state dir are gone
460++ self.assertFalse(os.path.isdir(backup))
461++ self.assertFalse(os.path.isdir(tmpdir))
462++
463+ def test_netplan_dbus_config_apply(self):
464+ cid = self._new_config_object()
465+ tmpdir = '/tmp/netplan-config-{}'.format(cid)
466++ backup = '/tmp/netplan-config-BACKUP'
467+ with open(os.path.join(tmpdir, 'etc', 'netplan', 'apply_test.yaml'), 'w') as f:
468+ f.write('TESTING-apply')
469+ with open(os.path.join(tmpdir, 'lib', 'netplan', 'apply_test.yaml'), 'w') as f:
470+@@ -400,7 +406,7 @@ class TestNetplanDBus(unittest.TestCase):
471+ ]
472+ out = subprocess.check_output(BUSCTL_NETPLAN_CMD)
473+ self.assertEqual(b'b true\n', out)
474+- self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "apply"]])
475++ self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "apply", "--state=/tmp/netplan-config-BACKUP"]])
476+ time.sleep(1) # Give some time for 'Apply' to clean up
477+ self.assertFalse(os.path.isdir(tmpdir))
478+
479+@@ -413,6 +419,10 @@ class TestNetplanDBus(unittest.TestCase):
480+ err = self._check_dbus_error(BUSCTL_NETPLAN_CMD)
481+ self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err)
482+
483++ # Verify the backup and config state dir are gone
484++ self.assertFalse(os.path.isdir(backup))
485++ self.assertFalse(os.path.isdir(tmpdir))
486++
487+ def test_netplan_dbus_config_try_cancel(self):
488+ # self-terminate after 30 dsec = 3 sec, if not cancelled before
489+ self.mock_netplan_cmd.set_timeout(30)
490+@@ -478,7 +488,8 @@ class TestNetplanDBus(unittest.TestCase):
491+ self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err)
492+
493+ # Verify 'netplan try' has been called
494+- self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=3"]])
495++ self.assertEquals(self.mock_netplan_cmd.calls(),
496++ [["netplan", "try", "--timeout=3", "--state=/tmp/netplan-config-BACKUP"]])
497+
498+ def test_netplan_dbus_config_try_cb(self):
499+ self.mock_netplan_cmd.set_timeout(1) # actually self-terminate after 0.1 sec
500+@@ -503,7 +514,7 @@ class TestNetplanDBus(unittest.TestCase):
501+ self.assertEqual(b'b true\n', out)
502+ time.sleep(1.5) # Give some time for the timeout to happen
503+
504+- # Verify the backup andconfig state dir are gone
505++ # Verify the backup and config state dir are gone
506+ self.assertFalse(os.path.isdir(backup))
507+ self.assertFalse(os.path.isdir(tmpdir))
508+
509+@@ -518,7 +529,8 @@ class TestNetplanDBus(unittest.TestCase):
510+ self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err)
511+
512+ # Verify 'netplan try' has been called
513+- self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=1"]])
514++ self.assertEquals(self.mock_netplan_cmd.calls(),
515++ [["netplan", "try", "--timeout=1", "--state=/tmp/netplan-config-BACKUP"]])
516+
517+ def test_netplan_dbus_config_try_apply(self):
518+ self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec
519+@@ -639,7 +651,7 @@ class TestNetplanDBus(unittest.TestCase):
520+ "--root-dir=/tmp/netplan-config-{}".format(cid)],
521+ ["netplan", "set", "ethernets.eth0.dhcp4=yes", "--origin-hint=70-snapd",
522+ "--root-dir=/tmp/netplan-config-{}".format(cid)],
523+- ["netplan", "apply"]
524++ ["netplan", "apply", "--state=/tmp/netplan-config-BACKUP"]
525+ ])
526+
527+ # Now it works again
528+@@ -756,7 +768,7 @@ class TestNetplanDBus(unittest.TestCase):
529+ self.assertEquals(self.mock_netplan_cmd.calls(), [
530+ ["netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd",
531+ "--root-dir=/tmp/netplan-config-{}".format(cid)],
532+- ["netplan", "try", "--timeout=1"],
533++ ["netplan", "try", "--timeout=1", "--state=/tmp/netplan-config-BACKUP"],
534+ ["netplan", "set", "ethernets.eth0.dhcp4=false", "--origin-hint=70-snapd",
535+ "--root-dir=/tmp/netplan-config-{}".format(cid2)]
536+ ])
537+diff --git a/tests/integration/base.py b/tests/integration/base.py
538+index 1054059..ec72d3c 100644
539+--- a/tests/integration/base.py
540++++ b/tests/integration/base.py
541+@@ -287,11 +287,14 @@ class IntegrationTestsBase(unittest.TestCase):
542+ if 'bond' not in iface:
543+ self.assertIn('state UP', out)
544+
545+- def generate_and_settle(self, wait_interfaces=None):
546++ def generate_and_settle(self, wait_interfaces=None, state_dir=None):
547+ '''Generate config, launch and settle NM and networkd'''
548+
549+ # regenerate netplan config
550+- out = subprocess.check_output(['netplan', 'apply'], stderr=subprocess.STDOUT, universal_newlines=True)
551++ cmd = ['netplan', 'apply']
552++ if state_dir:
553++ cmd = cmd + ['--state', state_dir]
554++ out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, universal_newlines=True)
555+ if 'Run \'systemctl daemon-reload\' to reload units.' in out:
556+ self.fail('systemd units changed without reload')
557+ # start NM so that we can verify that it does not manage anything
558+diff --git a/tests/integration/scenarios.py b/tests/integration/scenarios.py
559+index 93f8a4a..e37dd18 100644
560+--- a/tests/integration/scenarios.py
561++++ b/tests/integration/scenarios.py
562+@@ -22,8 +22,11 @@
563+ # You should have received a copy of the GNU General Public License
564+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
565+
566++import os
567++import shutil
568+ import sys
569+ import subprocess
570++import tempfile
571+ import unittest
572+
573+ from base import IntegrationTestsBase, test_backends
574+@@ -107,6 +110,30 @@ class _CommonTests():
575+ self.assert_iface_up(self.dev_e2_client, ['master br1'], ['inet '])
576+ self.assert_iface_up('bond0', ['master br0'])
577+
578++ # https://bugs.launchpad.net/netplan/+bug/1943120
579++ def test_remove_virtual_interfaces(self):
580++ tempdir = tempfile.mkdtemp()
581++ self.addCleanup(shutil.rmtree, tempdir)
582++ self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br54'], stderr=subprocess.DEVNULL)
583++ confdir = os.path.join(tempdir, 'etc', 'netplan')
584++ os.makedirs(confdir)
585++ with open(self.config, 'w') as f:
586++ f.write('''network:
587++ renderer: %(r)s
588++ version: 2
589++ bridges:
590++ br54:
591++ addresses: [1.2.3.4/24]''' % {'r': self.backend})
592++ self.generate_and_settle(['br54'])
593++ self.assert_iface('br54', ['inet 1.2.3.4/24'])
594++ # backup the current YAML state (incl. br54)
595++ shutil.copytree('/etc/netplan', confdir, dirs_exist_ok=True)
596++ # drop br54 interface
597++ subprocess.check_call(['netplan', 'set', 'network.bridges.br54.addresses=null'])
598++ self.generate_and_settle([], state_dir=tempdir)
599++ res = subprocess.run(['ip', 'link', 'show', 'dev', 'br54'], capture_output=True, text=True)
600++ self.assertIn('not exist', res.stderr)
601++
602+
603+ @unittest.skipIf("networkd" not in test_backends,
604+ "skipping as networkd backend tests are disabled")
605+diff --git a/tests/test_cli_units.py b/tests/test_cli_units.py
606+index 0814c18..90075e1 100644
607+--- a/tests/test_cli_units.py
608++++ b/tests/test_cli_units.py
609+@@ -18,7 +18,9 @@
610+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
611+
612+ import unittest
613++import subprocess
614+
615++from unittest.mock import patch
616+ from netplan.cli.commands.apply import NetplanApply
617+
618+
619+@@ -39,3 +41,41 @@ class TestCLI(unittest.TestCase):
620+ def test_is_composite_member_with_renderer(self):
621+ res = NetplanApply.is_composite_member([{'renderer': 'networkd', 'br0': {'interfaces': ['eth0']}}], 'eth0')
622+ self.assertTrue(res)
623++
624++ @patch('subprocess.check_call')
625++ def test_clear_virtual_links(self, mock):
626++ # simulate as if 'tun3' would have already been delete another way,
627++ # e.g. via NetworkManager backend
628++ res = NetplanApply.clear_virtual_links(['br0', 'vlan2', 'bond1', 'tun3'],
629++ ['br0', 'vlan2'],
630++ devices=['br0', 'vlan2', 'bond1', 'eth0'])
631++ mock.assert_called_with(['ip', 'link', 'delete', 'dev', 'bond1'])
632++ self.assertIn('bond1', res)
633++ self.assertIn('tun3', res)
634++ self.assertNotIn('br0', res)
635++ self.assertNotIn('vlan2', res)
636++
637++ @patch('subprocess.check_call')
638++ def test_clear_virtual_links_failure(self, mock):
639++ mock.side_effect = subprocess.CalledProcessError(1, '', 'Cannot find device "br0"')
640++ res = NetplanApply.clear_virtual_links(['br0'], [], devices=['br0', 'eth0'])
641++ mock.assert_called_with(['ip', 'link', 'delete', 'dev', 'br0'])
642++ self.assertIn('br0', res)
643++ self.assertNotIn('eth0', res)
644++
645++ @patch('subprocess.check_call')
646++ def test_clear_virtual_links_no_delta(self, mock):
647++ res = NetplanApply.clear_virtual_links(['br0', 'vlan2'],
648++ ['br0', 'vlan2'],
649++ devices=['br0', 'vlan2', 'eth0'])
650++ mock.assert_not_called()
651++ self.assertEquals(res, [])
652++
653++ @patch('subprocess.check_call')
654++ def test_clear_virtual_links_no_devices(self, mock):
655++ with self.assertLogs('', level='INFO') as ctx:
656++ res = NetplanApply.clear_virtual_links(['br0', 'br1'],
657++ ['br0'])
658++ self.assertEquals(res, [])
659++ self.assertEqual(ctx.output, ['WARNING:root:Cannot clear virtual links: no network interfaces provided.'])
660++ mock.assert_not_called()
661+diff --git a/tests/test_configmanager.py b/tests/test_configmanager.py
662+index e910f4e..625a1f1 100644
663+--- a/tests/test_configmanager.py
664++++ b/tests/test_configmanager.py
665+@@ -151,6 +151,12 @@ class TestConfigManager(unittest.TestCase):
666+ self.assertEquals(2, self.configmanager.version)
667+ self.assertEquals('networkd', self.configmanager.renderer)
668+ self.assertIn('fallback', self.configmanager.nm_devices)
669++ self.assertIn('vlan2', self.configmanager.virtual_interfaces)
670++ self.assertIn('br3', self.configmanager.virtual_interfaces)
671++ self.assertIn('br4', self.configmanager.virtual_interfaces)
672++ self.assertIn('bond5', self.configmanager.virtual_interfaces)
673++ self.assertIn('bond6', self.configmanager.virtual_interfaces)
674++ self.assertIn('he-ipv6', self.configmanager.virtual_interfaces)
675+
676+ def test_parse_merging(self):
677+ self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile_merging.yaml")])
678diff --git a/debian/patches/0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch b/debian/patches/0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch
679new file mode 100644
680index 0000000..50e9e1c
681--- /dev/null
682+++ b/debian/patches/0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch
683@@ -0,0 +1,368 @@
684+From: Simon Chopin <simon.chopin@canonical.com>
685+Date: Mon, 4 Oct 2021 14:18:50 +0200
686+Subject: netplan: set: make it possible to unset a whole devtype subtree (LP:
687+ #1942930) (FR-1685) (#236)
688+MIME-Version: 1.0
689+Content-Type: text/plain; charset="utf-8"
690+Content-Transfer-Encoding: 8bit
691+
692+BACKPORT (from upstream): moving names.c ENUM_FUNCTION logic into util.c
693+
694+Previously, trying to unset a whole devtype subtree, such as netplan set network.ethernets=null would fail. This PR fixes it by detecting this special case and manually deleting each netdef assigned to the subtype.
695+
696+This approach is far from being performant, but it has the merit of working without needing too much new code.
697+
698+COMMITS:
699+* tests: cli_get_set: use assertRaises to check for exceptions
700+This allows the test machinery to do some better error reporting when an
701+exception is raised, as it understands that the issue is an uncaught
702+exception, not a failed comparison.
703+
704+* lib: names: add a function to parse a string into a devtype
705+As I'm expecting this kind of function to be useful for the parser, the
706+implementation is directly in the form of a macro modelled after its
707+counterpart.
708+
709+* lib,netplan: tell Python about all the netdefs for a given devtype
710+This is implemented via iterators to limit the knowledge Python has of
711+the underlying memory model. The new symbols are prefixed with '_' to
712+denote that they are not to be used by public consumers.
713+
714+We also expose process_yaml_hierarchy which was already part of the ABI,
715+as it seems needed for the Python helper implementation.
716+
717+Contrary to the usual FFI calls, this one explicitly checks the
718+existence of the symbols and raises an exception if the symbols are not
719+found, as it is not totally unlikely that netplan.io and libnetplan0 be
720+upgraded out of sync.
721+
722+V2: rename the C internal iterator struct, which hadn't evolved along
723+with the successive iterations on the code
724+
725+fixup itar
726+
727+* netplan: set: allow the removal of an entire devtype subtree
728+See LP: #1942930
729+
730+Note that performance for this are horrendous, as we parse the whole
731+tree once for *each* removed netdef!
732+
733+V2:
734+* Lukas as co-author, as he wrote the test!
735+* Clean up commented-out code in the tests
736+
737+Co-authored-by: Lukas Märdian <lukas.maerdian@canonical.com>
738+---
739+ netplan/cli/commands/set.py | 9 +++++++-
740+ netplan/cli/utils.py | 46 +++++++++++++++++++++++++++++++++++++
741+ src/util.c | 55 ++++++++++++++++++++++++++++++++++++++++++++
742+ tests/test_cli_get_set.py | 56 ++++++++++++++++++++++++++++-----------------
743+ tests/test_utils.py | 33 ++++++++++++++++++++++++++
744+ 5 files changed, 177 insertions(+), 22 deletions(-)
745+
746+diff --git a/netplan/cli/commands/set.py b/netplan/cli/commands/set.py
747+index 3bf7dc6..2d75cac 100644
748+--- a/netplan/cli/commands/set.py
749++++ b/netplan/cli/commands/set.py
750+@@ -59,7 +59,14 @@ class NetplanSet(utils.NetplanCommand):
751+ for devtype in network:
752+ if devtype in GLOBAL_KEYS:
753+ continue # special handling of global keys down below
754+- for netdef in network.get(devtype, []):
755++ devtype_content = network.get(devtype, [])
756++ # Special case: removal of a whole devtype.
757++ # We replace the devtype null node with a dict of all defined netdefs
758++ # set to None.
759++ if devtype_content is None:
760++ devtype_content = {dev: None for dev in utils.netplan_get_ids_for_devtype(devtype, self.root_dir)}
761++ network[devtype] = devtype_content
762++ for netdef in devtype_content:
763+ hint = FALLBACK_HINT
764+ filename = utils.netplan_get_filename_by_id(netdef, self.root_dir)
765+ if filename:
766+diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py
767+index b0c87c9..656d60f 100644
768+--- a/netplan/cli/utils.py
769++++ b/netplan/cli/utils.py
770+@@ -32,6 +32,10 @@ NM_SERVICE_NAME = 'NetworkManager.service'
771+ NM_SNAP_SERVICE_NAME = 'snap.network-manager.networkmanager.service'
772+
773+
774++class LibNetplanException(Exception):
775++ pass
776++
777++
778+ class _GError(ctypes.Structure):
779+ _fields_ = [("domain", ctypes.c_uint32), ("code", ctypes.c_int), ("message", ctypes.c_char_p)]
780+
781+@@ -39,6 +43,8 @@ class _GError(ctypes.Structure):
782+ lib = ctypes.CDLL(ctypes.util.find_library('netplan'))
783+ lib.netplan_parse_yaml.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.POINTER(_GError))]
784+ lib.netplan_get_filename_by_id.restype = ctypes.c_char_p
785++lib.process_yaml_hierarchy.argtypes = [ctypes.c_char_p]
786++lib.process_yaml_hierarchy.restype = ctypes.c_int
787+
788+
789+ def netplan_parse(path):
790+@@ -59,6 +65,46 @@ def netplan_get_filename_by_id(netdef_id, rootdir):
791+ return res.decode('utf-8') if res else None
792+
793+
794++class _NetdefIdIterator:
795++ def __init__(self, devtype):
796++ self.iterator = lib._netplan_iter_defs_per_devtype_init(devtype.encode('utf-8'))
797++
798++ def __del__(self):
799++ lib._netplan_iter_defs_per_devtype_free(self.iterator)
800++
801++ def __iter__(self):
802++ return self
803++
804++ def __next__(self):
805++ next_value = lib._netplan_iter_defs_per_devtype_next(self.iterator)
806++ if next_value is None:
807++ raise StopIteration
808++ return next_value
809++
810++
811++def netplan_get_ids_for_devtype(devtype, rootdir):
812++ if not hasattr(lib, '_netplan_iter_defs_per_devtype_init'): # pragma: nocover (hard to unit-test against the WRONG lib)
813++ raise LibNetplanException('''
814++ The current version of libnetplan does not allow iterating by devtype.
815++ Please ensure that both the netplan CLI package and its library are up to date.
816++ ''')
817++ lib._netplan_iter_defs_per_devtype_init.argtypes = [ctypes.c_char_p]
818++ lib._netplan_iter_defs_per_devtype_init.restype = ctypes.c_void_p
819++
820++ lib._netplan_iter_defs_per_devtype_next.argtypes = [ctypes.c_void_p]
821++ lib._netplan_iter_defs_per_devtype_next.restype = ctypes.c_void_p
822++
823++ lib._netplan_iter_defs_per_devtype_free.argtypes = [ctypes.c_void_p]
824++ lib._netplan_iter_defs_per_devtype_free.restype = None
825++
826++ lib._netplan_netdef_id.argtypes = [ctypes.c_void_p]
827++ lib._netplan_netdef_id.restype = ctypes.c_char_p
828++
829++ lib.process_yaml_hierarchy(rootdir.encode('utf-8'))
830++ nds = list(_NetdefIdIterator(devtype))
831++ return [lib._netplan_netdef_id(nd).decode('utf-8') for nd in nds]
832++
833++
834+ def get_generator_path():
835+ return os.environ.get('NETPLAN_GENERATE_PATH', '/lib/netplan/generate')
836+
837+diff --git a/src/util.c b/src/util.c
838+index a4c0dba..4c8d716 100644
839+--- a/src/util.c
840++++ b/src/util.c
841+@@ -327,3 +327,58 @@ get_global_network(int ip_family)
842+ else
843+ return "::/0";
844+ }
845++
846++#define ENUM_FUNCTION(_radical, _type) _type netplan_ ## _radical ## _from_name(const char* val) \
847++{ \
848++ for (int i = 0; i < sizeof(netplan_ ## _radical ## _to_str); ++i) { \
849++ if (g_strcmp0(val, netplan_ ## _radical ## _to_str[i]) == 0) \
850++ return i; \
851++ } \
852++ return -1; \
853++}
854++
855++ENUM_FUNCTION(def_type, NetplanDefType);
856++
857++struct netdef_pertype_iterator {
858++ NetplanDefType type;
859++ GHashTableIter iter;
860++};
861++
862++struct netdef_pertype_iterator*
863++_netplan_iter_defs_per_devtype_init(const char *devtype)
864++{
865++ NetplanDefType type = netplan_def_type_from_name(devtype);
866++ struct netdef_pertype_iterator *iter = g_malloc0(sizeof(*iter));
867++ iter->type = type;
868++ if (netdefs)
869++ g_hash_table_iter_init(&iter->iter, netdefs);
870++ return iter;
871++}
872++
873++NetplanNetDefinition*
874++_netplan_iter_defs_per_devtype_next(struct netdef_pertype_iterator* it)
875++{
876++ gpointer key, value;
877++
878++ if (!netdefs)
879++ return NULL;
880++
881++ while (g_hash_table_iter_next(&it->iter, &key, &value)) {
882++ NetplanNetDefinition* netdef = value;
883++ if (netdef->type == it->type)
884++ return netdef;
885++ }
886++ return NULL;
887++}
888++
889++void
890++_netplan_iter_defs_per_devtype_free(struct netdef_pertype_iterator* it)
891++{
892++ g_free(it);
893++}
894++
895++const char*
896++_netplan_netdef_id(NetplanNetDefinition* nd)
897++{
898++ return nd->id;
899++}
900+diff --git a/tests/test_cli_get_set.py b/tests/test_cli_get_set.py
901+index 7a1799b..b5206e7 100644
902+--- a/tests/test_cli_get_set.py
903++++ b/tests/test_cli_get_set.py
904+@@ -31,13 +31,11 @@ from netplan.cli.core import Netplan
905+ def _call_cli(args):
906+ old_sys_argv = sys.argv
907+ sys.argv = [old_sys_argv[0]] + args
908++ f = io.StringIO()
909+ try:
910+- f = io.StringIO()
911+ with redirect_stdout(f):
912+ Netplan().main()
913+ return f.getvalue()
914+- except Exception as e:
915+- return e
916+ finally:
917+ sys.argv = old_sys_argv
918+
919+@@ -110,20 +108,20 @@ class TestSet(unittest.TestCase):
920+ self.assertEquals('network:\n ethernets:\n eth0:\n dhcp4: true\n', f.read())
921+
922+ def test_set_empty_origin_hint(self):
923+- err = self._set(['ethernets.eth0.dhcp4=true', '--origin-hint='])
924+- self.assertIsInstance(err, Exception)
925+- self.assertIn('Invalid/empty origin-hint', str(err))
926++ with self.assertRaises(Exception) as context:
927++ self._set(['ethernets.eth0.dhcp4=true', '--origin-hint='])
928++ self.assertTrue('Invalid/empty origin-hint' in str(context.exception))
929+
930+ def test_set_invalid(self):
931+- err = self._set(['xxx.yyy=abc'])
932+- self.assertIsInstance(err, Exception)
933+- self.assertIn('unknown key \'xxx\'\n xxx:\n', str(err))
934++ with self.assertRaises(Exception) as context:
935++ self._set(['xxx.yyy=abc'])
936++ self.assertIn('unknown key \'xxx\'\n xxx:\n', str(context.exception))
937+ self.assertFalse(os.path.isfile(self.path))
938+
939+ def test_set_invalid_validation(self):
940+- err = self._set(['ethernets.eth0.set-name=myif0'])
941+- self.assertIsInstance(err, Exception)
942+- self.assertIn('eth0: \'set-name:\' requires \'match:\' properties', str(err))
943++ with self.assertRaises(Exception) as context:
944++ self._set(['ethernets.eth0.set-name=myif0'])
945++ self.assertIn('eth0: \'set-name:\' requires \'match:\' properties', str(context.exception))
946+ self.assertFalse(os.path.isfile(self.path))
947+
948+ def test_set_invalid_validation2(self):
949+@@ -134,9 +132,9 @@ class TestSet(unittest.TestCase):
950+ mode: sit
951+ local: 1.2.3.4
952+ remote: 5.6.7.8''')
953+- err = self._set(['tunnels.tun0.keys.input=12345'])
954+- self.assertIsInstance(err, Exception)
955+- self.assertIn('tun0: \'input-key\' is not required for this tunnel type', str(err))
956++ with self.assertRaises(Exception) as context:
957++ self._set(['tunnels.tun0.keys.input=12345'])
958++ self.assertIn('tun0: \'input-key\' is not required for this tunnel type', str(context.exception))
959+
960+ def test_set_append(self):
961+ with open(self.path, 'w') as f:
962+@@ -195,6 +193,20 @@ class TestSet(unittest.TestCase):
963+ self.assertNotIn('addresses:', out)
964+ self.assertNotIn('eth0:', out)
965+
966++ def test_set_delete_subtree(self):
967++ with open(self.path, 'w') as f:
968++ f.write('''network:\n version: 2\n renderer: NetworkManager
969++ ethernets:
970++ eth0: {addresses: [1.2.3.4/24]}''')
971++ self._set(['network.ethernets=null'])
972++ self.assertTrue(os.path.isfile(self.path))
973++ with open(self.path, 'r') as f:
974++ out = f.read()
975++ self.assertIn('network:\n', out)
976++ self.assertIn(' version: 2\n', out)
977++ self.assertIn(' renderer: NetworkManager\n', out)
978++ self.assertNotIn('ethernets:', out)
979++
980+ def test_set_delete_file(self):
981+ with open(self.path, 'w') as f:
982+ f.write('''network:
983+@@ -220,9 +232,9 @@ class TestSet(unittest.TestCase):
984+ f.write('''network:\n version: 2\n renderer: NetworkManager
985+ ethernets:
986+ eth0: {addresses: [1.2.3.4]}''')
987+- err = self._set(['ethernets.eth0.addresses'])
988+- self.assertIsInstance(err, Exception)
989+- self.assertEquals('Invalid value specified', str(err))
990++ with self.assertRaises(Exception) as context:
991++ self._set(['ethernets.eth0.addresses'])
992++ self.assertEquals('Invalid value specified', str(context.exception))
993+
994+ def test_set_escaped_dot(self):
995+ self._set([r'ethernets.eth0\.123.dhcp4=false'])
996+@@ -231,9 +243,11 @@ class TestSet(unittest.TestCase):
997+ self.assertIn('network:\n ethernets:\n eth0.123:\n dhcp4: false', f.read())
998+
999+ def test_set_invalid_input(self):
1000+- err = self._set([r'ethernets.eth0={dhcp4:false}'])
1001+- self.assertIsInstance(err, Exception)
1002+- self.assertEquals('Invalid input: {\'network\': {\'ethernets\': {\'eth0\': {\'dhcp4:false\': None}}}}', str(err))
1003++ with self.assertRaises(Exception) as context:
1004++ self._set([r'ethernets.eth0={dhcp4:false}'])
1005++ self.assertEquals(
1006++ 'Invalid input: {\'network\': {\'ethernets\': {\'eth0\': {\'dhcp4:false\': None}}}}',
1007++ str(context.exception))
1008+
1009+ def test_set_override_existing_file(self):
1010+ override = os.path.join(self.workdir.name, 'etc', 'netplan', 'some-file.yaml')
1011+diff --git a/tests/test_utils.py b/tests/test_utils.py
1012+index 7954ec7..5c97ca2 100644
1013+--- a/tests/test_utils.py
1014++++ b/tests/test_utils.py
1015+@@ -196,3 +196,36 @@ class TestUtils(unittest.TestCase):
1016+ remote: 0.0.0.0
1017+ key: 0.0.0.0''')
1018+ self.assertIsNone(utils.netplan_get_filename_by_id('some-id', self.workdir.name))
1019++
1020++ def test_netplan_get_ids_for_devtype(self):
1021++ path = os.path.join(self.workdir.name, 'etc/netplan/a.yaml')
1022++ with open(path, 'w') as f:
1023++ f.write('''network:
1024++ ethernets:
1025++ id_b:
1026++ dhcp4: true
1027++ id_a:
1028++ dhcp4: true
1029++ vlans:
1030++ en-intra:
1031++ id: 3
1032++ link: id_b
1033++ dhcp4: true''')
1034++ self.assertSetEqual(
1035++ set(utils.netplan_get_ids_for_devtype("ethernets", self.workdir.name)),
1036++ set(["id_a", "id_b"]))
1037++
1038++ def test_netplan_get_ids_for_devtype_no_dev(self):
1039++ path = os.path.join(self.workdir.name, 'etc/netplan/a.yaml')
1040++ with open(path, 'w') as f:
1041++ f.write('''network:
1042++ ethernets:
1043++ id_b:
1044++ dhcp4: true''')
1045++ self.assertSetEqual(
1046++ set(utils.netplan_get_ids_for_devtype("tunnels", self.workdir.name)),
1047++ set([]))
1048++
1049++ def test_NetdefIdIterator_with_clear_netplan(self):
1050++ utils.lib.netplan_clear_netdefs()
1051++ self.assertSequenceEqual(list(utils._NetdefIdIterator("ethernets")), [])
1052diff --git a/debian/patches/series b/debian/patches/series
1053index 66b68b1..ba72f98 100644
1054--- a/debian/patches/series
1055+++ b/debian/patches/series
1056@@ -4,3 +4,5 @@
1057 0003-Mute-gateway4-6-deprecation-warnings.patch
1058 autopkgtest-fixes.patch
1059 nm-1.32.10-compat.patch
1060+0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch
1061+0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch
1062diff --git a/debian/tests/control b/debian/tests/control
1063index 8676b55..5453cf3 100644
1064--- a/debian/tests/control
1065+++ b/debian/tests/control
1066@@ -9,7 +9,7 @@ Depends: @,
1067 python3-gi,
1068 gir1.2-nm-1.0,
1069 openvswitch-switch,
1070-Restrictions: allow-stderr, needs-root, isolation-container, skip-not-installable
1071+Restrictions: allow-stderr, needs-root, isolation-machine, skip-not-installable, breaks-testbed
1072 Features: test-name=ovs
1073
1074 Test-Command: python3 tests/integration/run.py --test=ethernets
1075@@ -22,7 +22,7 @@ Depends: @,
1076 libnm0,
1077 python3-gi,
1078 gir1.2-nm-1.0,
1079-Restrictions: allow-stderr, needs-root, isolation-container
1080+Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed
1081 Features: test-name=ethernets
1082
1083 Test-Command: python3 tests/integration/run.py --test=bridges
1084@@ -35,7 +35,7 @@ Depends: @,
1085 libnm0,
1086 python3-gi,
1087 gir1.2-nm-1.0,
1088-Restrictions: allow-stderr, needs-root, isolation-container
1089+Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed
1090 Features: test-name=bridges
1091
1092 Test-Command: python3 tests/integration/run.py --test=bonds
1093@@ -48,7 +48,7 @@ Depends: @,
1094 libnm0,
1095 python3-gi,
1096 gir1.2-nm-1.0,
1097-Restrictions: allow-stderr, needs-root, isolation-container
1098+Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed
1099 Features: test-name=bonds
1100
1101 Test-Command: python3 tests/integration/run.py --test=routing
1102@@ -61,7 +61,7 @@ Depends: @,
1103 libnm0,
1104 python3-gi,
1105 gir1.2-nm-1.0,
1106-Restrictions: allow-stderr, needs-root, isolation-container
1107+Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed
1108 Features: test-name=routing
1109
1110 Test-Command: python3 tests/integration/run.py --test=vlans
1111@@ -74,7 +74,7 @@ Depends: @,
1112 libnm0,
1113 python3-gi,
1114 gir1.2-nm-1.0,
1115-Restrictions: allow-stderr, needs-root, isolation-container
1116+Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed
1117 Features: test-name=vlans
1118
1119 Test-Command: python3 tests/integration/run.py --test=wifi
1120@@ -87,7 +87,7 @@ Depends: @,
1121 libnm0,
1122 python3-gi,
1123 gir1.2-nm-1.0,
1124-Restrictions: allow-stderr, needs-root, isolation-container, flaky
1125+Restrictions: allow-stderr, needs-root, isolation-machine, flaky, breaks-testbed
1126 Features: test-name=wifi
1127
1128 Test-Command: python3 tests/integration/run.py --test=tunnels
1129@@ -101,7 +101,7 @@ Depends: @,
1130 python3-gi,
1131 gir1.2-nm-1.0,
1132 wireguard-tools,
1133-Restrictions: allow-stderr, needs-root, isolation-container
1134+Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed
1135 Features: test-name=tunnels
1136
1137 Test-Command: python3 tests/integration/run.py --test=scenarios
1138@@ -114,7 +114,7 @@ Depends: @,
1139 libnm0,
1140 python3-gi,
1141 gir1.2-nm-1.0,
1142-Restrictions: allow-stderr, needs-root, isolation-container
1143+Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed
1144 Features: test-name=scenarios
1145
1146 Test-Command: python3 tests/integration/run.py --test=regressions
1147@@ -127,11 +127,11 @@ Depends: @,
1148 libnm0,
1149 python3-gi,
1150 gir1.2-nm-1.0,
1151-Restrictions: allow-stderr, needs-root, isolation-container
1152+Restrictions: allow-stderr, needs-root, isolation-machine, breaks-testbed
1153 Features: test-name=regressions
1154
1155 Tests: autostart
1156-Restrictions: allow-stderr, needs-root, isolation-container
1157+Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed
1158
1159 Tests: cloud-init
1160-Restrictions: allow-stderr, needs-root, isolation-container
1161+Restrictions: allow-stderr, needs-root, isolation-container, breaks-testbed

Subscribers

People subscribed via source and target branches