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

Proposed by Simon Chopin
Status: Merged
Merge reported by: Lukas Märdian
Merged at revision: 1e04bd70211adac26275f1022ebe32e08f9cc1c0
Proposed branch: ~schopin/netplan/+git/ubuntu:hirsute-sru
Merge into: ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu/hirsute
Diff against target: 995 lines (+967/-0)
4 files modified
debian/changelog (+14/-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)
Reviewer Review Type Date Requested Status
Lukas Märdian Approve
Review via email: mp+409760@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Lukas Märdian (slyon) wrote (last edit ):

Thank you, LGTM!
Merged and sponsored.

I've removed the final dot "." at the end of the "Fix unset of a devtype subtree" line in d/changelog, to make it comply with lintian's 80 col limit (https://lintian.debian.org/tags/debian-changelog-line-too-long).

autopkgtest passed: https://autopkgtest.ubuntu.com/results/autopkgtest-hirsute-slyon-testing/hirsute/amd64/n/netplan.io/20211007_124048_49d33@/log.gz

review: Approve

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

Subscribers

People subscribed via source and target branches