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

Proposed by Simon Chopin
Status: Merged
Merged at revision: 9148efe070362ca027b96721447efed8d2cd7b79
Proposed branch: ~schopin/netplan/+git/ubuntu:focal-sru
Merge into: ~ubuntu-core-dev/netplan/+git/ubuntu:ubuntu/focal
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+409762@code.launchpad.net

This proposal supersedes a proposal from 2021-10-06.

To post a comment you must log in.
Revision history for this message
Lukas Märdian (slyon) : Posted in a previous version of this proposal
Revision history for this message
Lukas Märdian (slyon) wrote :

LGTM! And passes autopkgtest on focal/amd64:
https://autopkgtest.ubuntu.com/results/autopkgtest-focal-slyon-testing/focal/amd64/n/netplan.io/20211007_095503_a1d69@/log.gz

Just to be pedantic, 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).

$ gbp tag
gbp:info: Tagging Debian package 0.103-0ubuntu5~20.04.2 as ubuntu/0.103-0ubuntu5_20.04.2 in git

$ dput ubuntu ../netplan.io_0.103-0ubuntu5~20.04.2_source.changes
D: Setting host argument.
Checking signature on .changes
gpg: ../netplan.io_0.103-0ubuntu5~20.04.2_source.changes: Valid signature from 5889C17AB1C8D890
Checking signature on .dsc
gpg: ../netplan.io_0.103-0ubuntu5~20.04.2.dsc: Valid signature from 5889C17AB1C8D890
Uploading to ubuntu (via sftp to upload.ubuntu.com):
  Uploading netplan.io_0.103-0ubuntu5~20.04.2.dsc: done.
  Uploading netplan.io_0.103-0ubuntu5~20.04.2.debian.tar.xz: done.
  Uploading netplan.io_0.103-0ubuntu5~20.04.2_source.buildinfo: done.
  Uploading netplan.io_0.103-0ubuntu5~20.04.2_source.changes: done.
Successfully uploaded packages.

review: Approve

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
diff --git a/debian/changelog b/debian/changelog
index 0a1aeff..143fa1b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,17 @@
1netplan.io (0.103-0ubuntu5~20.04.2) focal; urgency=medium
2
3 * Backport patches from impish:
4 + Add d/p/0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch:
5 Fix unset of a devtype subtree, e.g. "netplan set network.ethernets=null".
6 (LP: #1942930)
7 + Add d/p/0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch:
8 Allow to pass a state to netplan apply/try so it can cleanup unused
9 virtual network interfaces after itself. Make use of this functionality
10 inside the DBus Config.Try()/Apply() API and the 'netplan try' CLI.
11 (LP: #1943120)
12
13 -- Simon Chopin <simon.chopin@canonical.com> Wed, 06 Oct 2021 12:57:35 +0200
14
1netplan.io (0.103-0ubuntu5~20.04.1) focal; urgency=medium15netplan.io (0.103-0ubuntu5~20.04.1) focal; urgency=medium
216
3 * Backport netplan.io 0.103-0ubuntu5 to 20.04 (LP: #1938920)17 * Backport netplan.io 0.103-0ubuntu5 to 20.04 (LP: #1938920)
diff --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
4new file mode 10064418new file mode 100644
index 0000000..a51ee6c
--- /dev/null
+++ b/debian/patches/0005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch
@@ -0,0 +1,583 @@
1From: =?utf-8?q?Lukas_M=C3=A4rdian?= <slyon@ubuntu.com>
2Date: Mon, 27 Sep 2021 16:40:45 +0200
3Subject: Implement YAML state tracking and use it in the DBus API and
4 netplan-try (LP: #1943120) (FR-1745) (#231)
5
6Allow 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).
7Netplan 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.
8
9The same functionality is used to roll back a netplan try command that failed or was rejected.
10
11Generally, 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.
12
13COMMITS:
14* cli:apply:configmanager: clear_virtual_links during apply
15* tests:scenarios: check virtual interface cleanup
16* doc:apply: update manpage
17* cli:try: use clear_virtual_links from Apply()
18* dbus: make use of new YAML state tracking
19* dbus: properly create and clean the backup state dir
20* cli:apply:clear_virtual_links: make devices a named parameter
21---
22 doc/netplan-apply.md | 18 ++++++-----
23 netplan/cli/commands/apply.py | 40 +++++++++++++++++++++++-
24 netplan/cli/commands/try_command.py | 28 ++++++++---------
25 netplan/configmanager.py | 10 ++++++
26 src/dbus.c | 62 ++++++++++++++++++++++++-------------
27 tests/dbus/test_dbus.py | 24 ++++++++++----
28 tests/integration/base.py | 7 +++--
29 tests/integration/scenarios.py | 27 ++++++++++++++++
30 tests/test_cli_units.py | 40 ++++++++++++++++++++++++
31 tests/test_configmanager.py | 6 ++++
32 10 files changed, 211 insertions(+), 51 deletions(-)
33
34diff --git a/doc/netplan-apply.md b/doc/netplan-apply.md
35index 153acb1..7811f04 100644
36--- a/doc/netplan-apply.md
37+++ b/doc/netplan-apply.md
38@@ -47,13 +47,17 @@ see **netplan**(5).
39
40 # KNOWN ISSUES
41
42-**netplan apply** will not remove virtual devices such as bridges
43-and bonds that have been created, even if they are no longer described
44-in the netplan configuration.
45-
46-This can be resolved by manually removing the virtual device (for
47-example ``ip link delete dev bond0``) and then running **netplan
48-apply**, or by rebooting.
49+**netplan apply** will not remove virtual devices such as bridges and bonds
50+that have been created, even if they are no longer described in the netplan
51+configuration. That is due to the fact that netplan operates statelessly and
52+is not aware of the previously defined virtal devices.
53+
54+This can be resolved by manually removing the virtual device (for example
55+``ip link delete dev bond0``) and then running **netplan apply**, by rebooting,
56+or by creating a temporary backup of the YAML state in ``/etc/netplan``
57+before modifying the configuration and passing this state to netplan (e.g.
58+``mkdir -p /tmp/netplan_state_backup/etc && cp -r /etc/netplan /tmp/netplan_state_backup/etc/``
59+then running **netplan apply --state /tmp/netplan_state_backup**)
60
61
62 # SEE ALSO
63diff --git a/netplan/cli/commands/apply.py b/netplan/cli/commands/apply.py
64index c3f5caa..477bc2f 100644
65--- a/netplan/cli/commands/apply.py
66+++ b/netplan/cli/commands/apply.py
67@@ -48,14 +48,18 @@ class NetplanApply(utils.NetplanCommand):
68 help='Only apply SR-IOV related configuration and exit')
69 self.parser.add_argument('--only-ovs-cleanup', action='store_true',
70 help='Only clean up old OpenVSwitch interfaces and exit')
71+ self.parser.add_argument('--state',
72+ help='Directory containing previous YAML configuration')
73
74 self.func = self.command_apply
75
76 self.parse_args()
77 self.run_command()
78
79- def command_apply(self, run_generate=True, sync=False, exit_on_error=True): # pragma: nocover (covered in autopkgtest)
80+ def command_apply(self, run_generate=True, sync=False, exit_on_error=True, state_dir=None): # pragma: nocover
81 config_manager = ConfigManager()
82+ if state_dir:
83+ self.state = state_dir
84
85 # For certain use-cases, we might want to only apply specific configuration.
86 # If we only need SR-IOV configuration, do that and exit early.
87@@ -190,6 +194,14 @@ class NetplanApply(utils.NetplanCommand):
88 # for now, only applies to non-virtual (real) devices.
89 config_manager.parse()
90 changes = NetplanApply.process_link_changes(devices, config_manager)
91+ # delete virtual interfaces that have been defined in a previous state
92+ # but are not configured anymore in the current YAML
93+ if self.state:
94+ cm = ConfigManager(self.state)
95+ cm.parse() # get previous configuration state
96+ prev_links = cm.virtual_interfaces.keys()
97+ curr_links = config_manager.virtual_interfaces.keys()
98+ NetplanApply.clear_virtual_links(prev_links, curr_links, devices)
99
100 # if the interface is up, we can still apply some .link file changes
101 # but we cannot apply the interface rename via udev, as it won't touch
102@@ -259,6 +271,32 @@ class NetplanApply(utils.NetplanCommand):
103
104 return False
105
106+ @staticmethod
107+ def clear_virtual_links(prev_links, curr_links, devices=[]):
108+ """
109+ Calculate the delta of virtual links. And remove the links that were
110+ dropped from the YAML config, if they were not dropped by the backend
111+ already.
112+ We can make use of the netplan netdef ids, as those equal the interface
113+ name for virtual links.
114+ """
115+ if not devices:
116+ logging.warning('Cannot clear virtual links: no network interfaces provided.')
117+ return []
118+
119+ dropped_interfaces = list(set(prev_links) - set(curr_links))
120+ # some interfaces might have been cleaned up already, e.g. by the
121+ # NetworkManager backend
122+ interfaces_to_clear = list(set(dropped_interfaces).intersection(devices))
123+ for link in interfaces_to_clear:
124+ try:
125+ cmd = ['ip', 'link', 'delete', 'dev', link]
126+ subprocess.check_call(cmd)
127+ except subprocess.CalledProcessError:
128+ logging.warn('Could not delete interface {}'.format(link))
129+
130+ return dropped_interfaces
131+
132 @staticmethod
133 def process_link_changes(interfaces, config_manager): # pragma: nocover (covered in autopkgtest)
134 """
135diff --git a/netplan/cli/commands/try_command.py b/netplan/cli/commands/try_command.py
136index 198992f..5534596 100644
137--- a/netplan/cli/commands/try_command.py
138+++ b/netplan/cli/commands/try_command.py
139@@ -19,10 +19,11 @@
140
141 import os
142 import time
143+import shutil
144 import signal
145 import sys
146+import tempfile
147 import logging
148-import subprocess
149
150 from netplan.configmanager import ConfigManager
151 import netplan.cli.utils as utils
152@@ -59,6 +60,8 @@ class NetplanTry(utils.NetplanCommand):
153 self.parser.add_argument('--timeout',
154 type=int, default=DEFAULT_INPUT_TIMEOUT,
155 help="Maximum number of seconds to wait for the user's confirmation")
156+ self.parser.add_argument('--state',
157+ help='Directory containing previous YAML configuration')
158
159 self.func = self.command_try
160
161@@ -81,7 +84,7 @@ class NetplanTry(utils.NetplanCommand):
162 self.backup()
163 self.setup()
164
165- NetplanApply().command_apply(run_generate=True, sync=True, exit_on_error=False)
166+ NetplanApply().command_apply(run_generate=True, sync=True, exit_on_error=False, state_dir=self.state)
167
168 self.t.get_confirmation_input(timeout=self.timeout)
169 except netplan.terminal.InputRejected:
170@@ -114,19 +117,16 @@ class NetplanTry(utils.NetplanCommand):
171 self.configuration_changed = True
172
173 def revert(self): # pragma: nocover (requires user input)
174+ # backup the state we just tried to apply
175+ tempdir = tempfile.mkdtemp()
176+ confdir = os.path.join(tempdir, 'etc', 'netplan')
177+ os.makedirs(confdir)
178+ shutil.copytree('/etc/netplan', confdir, dirs_exist_ok=True)
179+ # restore previous state
180 self.config_manager.revert()
181- NetplanApply().command_apply(run_generate=False, sync=True, exit_on_error=False)
182- for ifname in self.new_interfaces:
183- if ifname not in self.config_manager.bonds and \
184- ifname not in self.config_manager.bridges and \
185- ifname not in self.config_manager.vlans:
186- logging.debug("{} will not be removed: not a virtual interface".format(ifname))
187- continue
188- try:
189- cmd = ['ip', 'link', 'del', ifname]
190- subprocess.check_call(cmd)
191- except subprocess.CalledProcessError:
192- logging.warn("Could not revert (remove) new interface '{}'".format(ifname))
193+ NetplanApply().command_apply(run_generate=False, sync=True, exit_on_error=False, state_dir=tempdir)
194+ # clear the backup
195+ shutil.rmtree(tempdir)
196
197 def cleanup(self): # pragma: nocover (requires user input)
198 self.config_manager.cleanup()
199diff --git a/netplan/configmanager.py b/netplan/configmanager.py
200index 9278d04..133dc5b 100644
201--- a/netplan/configmanager.py
202+++ b/netplan/configmanager.py
203@@ -62,6 +62,16 @@ class ConfigManager(object):
204 interfaces.update(self.wifis)
205 return interfaces
206
207+ @property
208+ def virtual_interfaces(self):
209+ interfaces = {}
210+ # what about ovs_ports?
211+ interfaces.update(self.bridges)
212+ interfaces.update(self.bonds)
213+ interfaces.update(self.tunnels)
214+ interfaces.update(self.vlans)
215+ return interfaces
216+
217 @property
218 def ovs_ports(self):
219 return self.network['ovs_ports']
220diff --git a/src/dbus.c b/src/dbus.c
221index f0aa53a..a486b54 100644
222--- a/src/dbus.c
223+++ b/src/dbus.c
224@@ -199,6 +199,30 @@ _clear_tmp_state(const char *config_id, NetplanData *d)
225 return TRUE;
226 }
227
228+static int
229+_backup_global_state(sd_bus_error *ret_error)
230+{
231+ int r = 0;
232+ g_autofree gchar *path = NULL;
233+ path = g_strdup_printf("%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG);
234+ /* Create {etc,run,lib} subdirs with owner r/w permissions */
235+ char *subdir = NULL;
236+ for (int i = 0; i < 3; i++) {
237+ subdir = g_strdup_printf("%s/%s/netplan", path, NETPLAN_SUBDIRS[i]);
238+ r = g_mkdir_with_parents(subdir, 0700);
239+ if (r < 0)
240+ // LCOV_EXCL_START
241+ return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED,
242+ "Failed to create '%s': %s\n", subdir, strerror(errno));
243+ // LCOV_EXCL_STOP
244+ g_free(subdir);
245+ }
246+
247+ /* Copy main *.yaml files from /{etc,run,lib}/netplan/ to GLOBAL backup dir */
248+ _copy_yaml_state(NETPLAN_ROOT, path, ret_error);
249+ return 0;
250+}
251+
252 /**
253 * io.netplan.Netplan methods
254 */
255@@ -209,6 +233,7 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
256 g_autoptr(GError) err = NULL;
257 g_autofree gchar *stdout = NULL;
258 g_autofree gchar *stderr = NULL;
259+ g_autofree gchar *state = NULL;
260 gint exit_status = 0;
261 NetplanData *d = userdata;
262
263@@ -216,8 +241,9 @@ method_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
264 * Otherwise execute 'netplan apply' directly. */
265 if (d->try_pid > 0)
266 return _try_accept(TRUE, m, userdata, ret_error);
267-
268- gchar *argv[] = {SBINDIR "/" "netplan", "apply", NULL};
269+ if (d->config_id)
270+ state = g_strdup_printf("--state=%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG);
271+ gchar *argv[] = {SBINDIR "/" "netplan", "apply", state, NULL};
272
273 // for tests only: allow changing what netplan to run
274 if (getenv("DBUS_TEST_NETPLAN_CMD") != 0)
275@@ -421,6 +447,7 @@ method_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
276 {
277 g_autoptr(GError) err = NULL;
278 g_autofree gchar *timeout = NULL;
279+ g_autofree gchar *state = NULL;
280 gint child_stdin = -1; /* child process needs an input to function correctly */
281 guint seconds = 0;
282 int r = -1;
283@@ -430,7 +457,9 @@ method_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
284 return sd_bus_error_setf(ret_error, SD_BUS_ERROR_FAILED, "cannot extract timeout_seconds"); // LCOV_EXCL_LINE
285 if (seconds > 0)
286 timeout = g_strdup_printf("--timeout=%u", seconds);
287- gchar *argv[] = {SBINDIR "/" "netplan", "try", timeout, NULL};
288+ if (d->config_id)
289+ state = g_strdup_printf("--state=%s/netplan-config-%s", g_get_tmp_dir(), NETPLAN_GLOBAL_CONFIG);
290+ gchar *argv[] = {SBINDIR "/" "netplan", "try", timeout, state, NULL};
291
292 // for tests only: allow changing what netplan to run
293 if (getenv("DBUS_TEST_NETPLAN_CMD") != 0)
294@@ -482,6 +511,10 @@ method_config_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
295 d->config_dirty = g_strdup(d->config_id);
296
297 if (d->try_pid < 0) {
298+ r = _backup_global_state(ret_error);
299+ if (r < 0)
300+ return r; // LCOV_EXCL_LINE
301+
302 /* Delete GLOBAL state */
303 unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml");
304 /* Copy current config state to GLOBAL */
305@@ -491,6 +524,8 @@ method_config_apply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
306 }
307
308 r = method_apply(m, d, ret_error);
309+ /* Clear GLOBAL backup and config state */
310+ _clear_tmp_state(NETPLAN_GLOBAL_CONFIG, d);
311 _clear_tmp_state(d->config_id, d);
312
313 /* unlock current config ID and handler ID */
314@@ -535,7 +570,6 @@ static int
315 method_config_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
316 {
317 NetplanData *d = userdata;
318- g_autofree gchar *path = NULL;
319 g_autofree gchar *state_dir = NULL;
320 const char *config_id = sd_bus_message_get_path(m) + 27;
321 if (d->try_pid > 0)
322@@ -551,23 +585,9 @@ method_config_try(sd_bus_message *m, void *userdata, sd_bus_error *ret_error)
323 d->try_pid = G_MAXINT;
324 d->config_id = config_id;
325
326- /* Backup GLOBAL state */
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+ r = _backup_global_state(ret_error);
344+ if (r < 0)
345+ return r; // LCOV_EXCL_LINE
346
347 /* Clear main *.yaml files */
348 unlink_glob(NETPLAN_ROOT, "/{etc,run,lib}/netplan/*.yaml");
349diff --git a/tests/dbus/test_dbus.py b/tests/dbus/test_dbus.py
350index 87abf22..5c2f5b7 100644
351--- a/tests/dbus/test_dbus.py
352+++ b/tests/dbus/test_dbus.py
353@@ -361,6 +361,7 @@ class TestNetplanDBus(unittest.TestCase):
354 def test_netplan_dbus_config_cancel(self):
355 cid = self._new_config_object()
356 tmpdir = '/tmp/netplan-config-{}'.format(cid)
357+ backup = '/tmp/netplan-config-BACKUP'
358
359 # Verify .Config.Cancel() teardown of the config object and state dirs
360 BUSCTL_NETPLAN_CMD = [
361@@ -380,9 +381,14 @@ class TestNetplanDBus(unittest.TestCase):
362 err = self._check_dbus_error(BUSCTL_NETPLAN_CMD)
363 self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err)
364
365+ # Verify the backup and config state dir are gone
366+ self.assertFalse(os.path.isdir(backup))
367+ self.assertFalse(os.path.isdir(tmpdir))
368+
369 def test_netplan_dbus_config_apply(self):
370 cid = self._new_config_object()
371 tmpdir = '/tmp/netplan-config-{}'.format(cid)
372+ backup = '/tmp/netplan-config-BACKUP'
373 with open(os.path.join(tmpdir, 'etc', 'netplan', 'apply_test.yaml'), 'w') as f:
374 f.write('TESTING-apply')
375 with open(os.path.join(tmpdir, 'lib', 'netplan', 'apply_test.yaml'), 'w') as f:
376@@ -400,7 +406,7 @@ class TestNetplanDBus(unittest.TestCase):
377 ]
378 out = subprocess.check_output(BUSCTL_NETPLAN_CMD)
379 self.assertEqual(b'b true\n', out)
380- self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "apply"]])
381+ self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "apply", "--state=/tmp/netplan-config-BACKUP"]])
382 time.sleep(1) # Give some time for 'Apply' to clean up
383 self.assertFalse(os.path.isdir(tmpdir))
384
385@@ -413,6 +419,10 @@ class TestNetplanDBus(unittest.TestCase):
386 err = self._check_dbus_error(BUSCTL_NETPLAN_CMD)
387 self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err)
388
389+ # Verify the backup and config state dir are gone
390+ self.assertFalse(os.path.isdir(backup))
391+ self.assertFalse(os.path.isdir(tmpdir))
392+
393 def test_netplan_dbus_config_try_cancel(self):
394 # self-terminate after 30 dsec = 3 sec, if not cancelled before
395 self.mock_netplan_cmd.set_timeout(30)
396@@ -478,7 +488,8 @@ class TestNetplanDBus(unittest.TestCase):
397 self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err)
398
399 # Verify 'netplan try' has been called
400- self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=3"]])
401+ self.assertEquals(self.mock_netplan_cmd.calls(),
402+ [["netplan", "try", "--timeout=3", "--state=/tmp/netplan-config-BACKUP"]])
403
404 def test_netplan_dbus_config_try_cb(self):
405 self.mock_netplan_cmd.set_timeout(1) # actually self-terminate after 0.1 sec
406@@ -503,7 +514,7 @@ class TestNetplanDBus(unittest.TestCase):
407 self.assertEqual(b'b true\n', out)
408 time.sleep(1.5) # Give some time for the timeout to happen
409
410- # Verify the backup andconfig state dir are gone
411+ # Verify the backup and config state dir are gone
412 self.assertFalse(os.path.isdir(backup))
413 self.assertFalse(os.path.isdir(tmpdir))
414
415@@ -518,7 +529,8 @@ class TestNetplanDBus(unittest.TestCase):
416 self.assertIn('Unknown object \'/io/netplan/Netplan/config/{}\''.format(cid), err)
417
418 # Verify 'netplan try' has been called
419- self.assertEquals(self.mock_netplan_cmd.calls(), [["netplan", "try", "--timeout=1"]])
420+ self.assertEquals(self.mock_netplan_cmd.calls(),
421+ [["netplan", "try", "--timeout=1", "--state=/tmp/netplan-config-BACKUP"]])
422
423 def test_netplan_dbus_config_try_apply(self):
424 self.mock_netplan_cmd.set_timeout(30) # 30 dsec = 3 sec
425@@ -639,7 +651,7 @@ class TestNetplanDBus(unittest.TestCase):
426 "--root-dir=/tmp/netplan-config-{}".format(cid)],
427 ["netplan", "set", "ethernets.eth0.dhcp4=yes", "--origin-hint=70-snapd",
428 "--root-dir=/tmp/netplan-config-{}".format(cid)],
429- ["netplan", "apply"]
430+ ["netplan", "apply", "--state=/tmp/netplan-config-BACKUP"]
431 ])
432
433 # Now it works again
434@@ -756,7 +768,7 @@ class TestNetplanDBus(unittest.TestCase):
435 self.assertEquals(self.mock_netplan_cmd.calls(), [
436 ["netplan", "set", "ethernets.eth0.dhcp4=true", "--origin-hint=70-snapd",
437 "--root-dir=/tmp/netplan-config-{}".format(cid)],
438- ["netplan", "try", "--timeout=1"],
439+ ["netplan", "try", "--timeout=1", "--state=/tmp/netplan-config-BACKUP"],
440 ["netplan", "set", "ethernets.eth0.dhcp4=false", "--origin-hint=70-snapd",
441 "--root-dir=/tmp/netplan-config-{}".format(cid2)]
442 ])
443diff --git a/tests/integration/base.py b/tests/integration/base.py
444index 1054059..ec72d3c 100644
445--- a/tests/integration/base.py
446+++ b/tests/integration/base.py
447@@ -287,11 +287,14 @@ class IntegrationTestsBase(unittest.TestCase):
448 if 'bond' not in iface:
449 self.assertIn('state UP', out)
450
451- def generate_and_settle(self, wait_interfaces=None):
452+ def generate_and_settle(self, wait_interfaces=None, state_dir=None):
453 '''Generate config, launch and settle NM and networkd'''
454
455 # regenerate netplan config
456- out = subprocess.check_output(['netplan', 'apply'], stderr=subprocess.STDOUT, universal_newlines=True)
457+ cmd = ['netplan', 'apply']
458+ if state_dir:
459+ cmd = cmd + ['--state', state_dir]
460+ out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, universal_newlines=True)
461 if 'Run \'systemctl daemon-reload\' to reload units.' in out:
462 self.fail('systemd units changed without reload')
463 # start NM so that we can verify that it does not manage anything
464diff --git a/tests/integration/scenarios.py b/tests/integration/scenarios.py
465index 93f8a4a..e37dd18 100644
466--- a/tests/integration/scenarios.py
467+++ b/tests/integration/scenarios.py
468@@ -22,8 +22,11 @@
469 # You should have received a copy of the GNU General Public License
470 # along with this program. If not, see <http://www.gnu.org/licenses/>.
471
472+import os
473+import shutil
474 import sys
475 import subprocess
476+import tempfile
477 import unittest
478
479 from base import IntegrationTestsBase, test_backends
480@@ -107,6 +110,30 @@ class _CommonTests():
481 self.assert_iface_up(self.dev_e2_client, ['master br1'], ['inet '])
482 self.assert_iface_up('bond0', ['master br0'])
483
484+ # https://bugs.launchpad.net/netplan/+bug/1943120
485+ def test_remove_virtual_interfaces(self):
486+ tempdir = tempfile.mkdtemp()
487+ self.addCleanup(shutil.rmtree, tempdir)
488+ self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br54'], stderr=subprocess.DEVNULL)
489+ confdir = os.path.join(tempdir, 'etc', 'netplan')
490+ os.makedirs(confdir)
491+ with open(self.config, 'w') as f:
492+ f.write('''network:
493+ renderer: %(r)s
494+ version: 2
495+ bridges:
496+ br54:
497+ addresses: [1.2.3.4/24]''' % {'r': self.backend})
498+ self.generate_and_settle(['br54'])
499+ self.assert_iface('br54', ['inet 1.2.3.4/24'])
500+ # backup the current YAML state (incl. br54)
501+ shutil.copytree('/etc/netplan', confdir, dirs_exist_ok=True)
502+ # drop br54 interface
503+ subprocess.check_call(['netplan', 'set', 'network.bridges.br54.addresses=null'])
504+ self.generate_and_settle([], state_dir=tempdir)
505+ res = subprocess.run(['ip', 'link', 'show', 'dev', 'br54'], capture_output=True, text=True)
506+ self.assertIn('not exist', res.stderr)
507+
508
509 @unittest.skipIf("networkd" not in test_backends,
510 "skipping as networkd backend tests are disabled")
511diff --git a/tests/test_cli_units.py b/tests/test_cli_units.py
512index 0814c18..90075e1 100644
513--- a/tests/test_cli_units.py
514+++ b/tests/test_cli_units.py
515@@ -18,7 +18,9 @@
516 # along with this program. If not, see <http://www.gnu.org/licenses/>.
517
518 import unittest
519+import subprocess
520
521+from unittest.mock import patch
522 from netplan.cli.commands.apply import NetplanApply
523
524
525@@ -39,3 +41,41 @@ class TestCLI(unittest.TestCase):
526 def test_is_composite_member_with_renderer(self):
527 res = NetplanApply.is_composite_member([{'renderer': 'networkd', 'br0': {'interfaces': ['eth0']}}], 'eth0')
528 self.assertTrue(res)
529+
530+ @patch('subprocess.check_call')
531+ def test_clear_virtual_links(self, mock):
532+ # simulate as if 'tun3' would have already been delete another way,
533+ # e.g. via NetworkManager backend
534+ res = NetplanApply.clear_virtual_links(['br0', 'vlan2', 'bond1', 'tun3'],
535+ ['br0', 'vlan2'],
536+ devices=['br0', 'vlan2', 'bond1', 'eth0'])
537+ mock.assert_called_with(['ip', 'link', 'delete', 'dev', 'bond1'])
538+ self.assertIn('bond1', res)
539+ self.assertIn('tun3', res)
540+ self.assertNotIn('br0', res)
541+ self.assertNotIn('vlan2', res)
542+
543+ @patch('subprocess.check_call')
544+ def test_clear_virtual_links_failure(self, mock):
545+ mock.side_effect = subprocess.CalledProcessError(1, '', 'Cannot find device "br0"')
546+ res = NetplanApply.clear_virtual_links(['br0'], [], devices=['br0', 'eth0'])
547+ mock.assert_called_with(['ip', 'link', 'delete', 'dev', 'br0'])
548+ self.assertIn('br0', res)
549+ self.assertNotIn('eth0', res)
550+
551+ @patch('subprocess.check_call')
552+ def test_clear_virtual_links_no_delta(self, mock):
553+ res = NetplanApply.clear_virtual_links(['br0', 'vlan2'],
554+ ['br0', 'vlan2'],
555+ devices=['br0', 'vlan2', 'eth0'])
556+ mock.assert_not_called()
557+ self.assertEquals(res, [])
558+
559+ @patch('subprocess.check_call')
560+ def test_clear_virtual_links_no_devices(self, mock):
561+ with self.assertLogs('', level='INFO') as ctx:
562+ res = NetplanApply.clear_virtual_links(['br0', 'br1'],
563+ ['br0'])
564+ self.assertEquals(res, [])
565+ self.assertEqual(ctx.output, ['WARNING:root:Cannot clear virtual links: no network interfaces provided.'])
566+ mock.assert_not_called()
567diff --git a/tests/test_configmanager.py b/tests/test_configmanager.py
568index e910f4e..625a1f1 100644
569--- a/tests/test_configmanager.py
570+++ b/tests/test_configmanager.py
571@@ -151,6 +151,12 @@ class TestConfigManager(unittest.TestCase):
572 self.assertEquals(2, self.configmanager.version)
573 self.assertEquals('networkd', self.configmanager.renderer)
574 self.assertIn('fallback', self.configmanager.nm_devices)
575+ self.assertIn('vlan2', self.configmanager.virtual_interfaces)
576+ self.assertIn('br3', self.configmanager.virtual_interfaces)
577+ self.assertIn('br4', self.configmanager.virtual_interfaces)
578+ self.assertIn('bond5', self.configmanager.virtual_interfaces)
579+ self.assertIn('bond6', self.configmanager.virtual_interfaces)
580+ self.assertIn('he-ipv6', self.configmanager.virtual_interfaces)
581
582 def test_parse_merging(self):
583 self.configmanager.parse(extra_config=[os.path.join(self.workdir.name, "newfile_merging.yaml")])
diff --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
0new file mode 100644584new file mode 100644
index 0000000..50e9e1c
--- /dev/null
+++ b/debian/patches/0006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch
@@ -0,0 +1,368 @@
1From: Simon Chopin <simon.chopin@canonical.com>
2Date: Mon, 4 Oct 2021 14:18:50 +0200
3Subject: netplan: set: make it possible to unset a whole devtype subtree (LP:
4 #1942930) (FR-1685) (#236)
5MIME-Version: 1.0
6Content-Type: text/plain; charset="utf-8"
7Content-Transfer-Encoding: 8bit
8
9BACKPORT (from upstream): moving names.c ENUM_FUNCTION logic into util.c
10
11Previously, 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.
12
13This approach is far from being performant, but it has the merit of working without needing too much new code.
14
15COMMITS:
16* tests: cli_get_set: use assertRaises to check for exceptions
17This allows the test machinery to do some better error reporting when an
18exception is raised, as it understands that the issue is an uncaught
19exception, not a failed comparison.
20
21* lib: names: add a function to parse a string into a devtype
22As I'm expecting this kind of function to be useful for the parser, the
23implementation is directly in the form of a macro modelled after its
24counterpart.
25
26* lib,netplan: tell Python about all the netdefs for a given devtype
27This is implemented via iterators to limit the knowledge Python has of
28the underlying memory model. The new symbols are prefixed with '_' to
29denote that they are not to be used by public consumers.
30
31We also expose process_yaml_hierarchy which was already part of the ABI,
32as it seems needed for the Python helper implementation.
33
34Contrary to the usual FFI calls, this one explicitly checks the
35existence of the symbols and raises an exception if the symbols are not
36found, as it is not totally unlikely that netplan.io and libnetplan0 be
37upgraded out of sync.
38
39V2: rename the C internal iterator struct, which hadn't evolved along
40with the successive iterations on the code
41
42fixup itar
43
44* netplan: set: allow the removal of an entire devtype subtree
45See LP: #1942930
46
47Note that performance for this are horrendous, as we parse the whole
48tree once for *each* removed netdef!
49
50V2:
51* Lukas as co-author, as he wrote the test!
52* Clean up commented-out code in the tests
53
54Co-authored-by: Lukas Märdian <lukas.maerdian@canonical.com>
55---
56 netplan/cli/commands/set.py | 9 +++++++-
57 netplan/cli/utils.py | 46 +++++++++++++++++++++++++++++++++++++
58 src/util.c | 55 ++++++++++++++++++++++++++++++++++++++++++++
59 tests/test_cli_get_set.py | 56 ++++++++++++++++++++++++++++-----------------
60 tests/test_utils.py | 33 ++++++++++++++++++++++++++
61 5 files changed, 177 insertions(+), 22 deletions(-)
62
63diff --git a/netplan/cli/commands/set.py b/netplan/cli/commands/set.py
64index 3bf7dc6..2d75cac 100644
65--- a/netplan/cli/commands/set.py
66+++ b/netplan/cli/commands/set.py
67@@ -59,7 +59,14 @@ class NetplanSet(utils.NetplanCommand):
68 for devtype in network:
69 if devtype in GLOBAL_KEYS:
70 continue # special handling of global keys down below
71- for netdef in network.get(devtype, []):
72+ devtype_content = network.get(devtype, [])
73+ # Special case: removal of a whole devtype.
74+ # We replace the devtype null node with a dict of all defined netdefs
75+ # set to None.
76+ if devtype_content is None:
77+ devtype_content = {dev: None for dev in utils.netplan_get_ids_for_devtype(devtype, self.root_dir)}
78+ network[devtype] = devtype_content
79+ for netdef in devtype_content:
80 hint = FALLBACK_HINT
81 filename = utils.netplan_get_filename_by_id(netdef, self.root_dir)
82 if filename:
83diff --git a/netplan/cli/utils.py b/netplan/cli/utils.py
84index b0c87c9..656d60f 100644
85--- a/netplan/cli/utils.py
86+++ b/netplan/cli/utils.py
87@@ -32,6 +32,10 @@ NM_SERVICE_NAME = 'NetworkManager.service'
88 NM_SNAP_SERVICE_NAME = 'snap.network-manager.networkmanager.service'
89
90
91+class LibNetplanException(Exception):
92+ pass
93+
94+
95 class _GError(ctypes.Structure):
96 _fields_ = [("domain", ctypes.c_uint32), ("code", ctypes.c_int), ("message", ctypes.c_char_p)]
97
98@@ -39,6 +43,8 @@ class _GError(ctypes.Structure):
99 lib = ctypes.CDLL(ctypes.util.find_library('netplan'))
100 lib.netplan_parse_yaml.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.POINTER(_GError))]
101 lib.netplan_get_filename_by_id.restype = ctypes.c_char_p
102+lib.process_yaml_hierarchy.argtypes = [ctypes.c_char_p]
103+lib.process_yaml_hierarchy.restype = ctypes.c_int
104
105
106 def netplan_parse(path):
107@@ -59,6 +65,46 @@ def netplan_get_filename_by_id(netdef_id, rootdir):
108 return res.decode('utf-8') if res else None
109
110
111+class _NetdefIdIterator:
112+ def __init__(self, devtype):
113+ self.iterator = lib._netplan_iter_defs_per_devtype_init(devtype.encode('utf-8'))
114+
115+ def __del__(self):
116+ lib._netplan_iter_defs_per_devtype_free(self.iterator)
117+
118+ def __iter__(self):
119+ return self
120+
121+ def __next__(self):
122+ next_value = lib._netplan_iter_defs_per_devtype_next(self.iterator)
123+ if next_value is None:
124+ raise StopIteration
125+ return next_value
126+
127+
128+def netplan_get_ids_for_devtype(devtype, rootdir):
129+ if not hasattr(lib, '_netplan_iter_defs_per_devtype_init'): # pragma: nocover (hard to unit-test against the WRONG lib)
130+ raise LibNetplanException('''
131+ The current version of libnetplan does not allow iterating by devtype.
132+ Please ensure that both the netplan CLI package and its library are up to date.
133+ ''')
134+ lib._netplan_iter_defs_per_devtype_init.argtypes = [ctypes.c_char_p]
135+ lib._netplan_iter_defs_per_devtype_init.restype = ctypes.c_void_p
136+
137+ lib._netplan_iter_defs_per_devtype_next.argtypes = [ctypes.c_void_p]
138+ lib._netplan_iter_defs_per_devtype_next.restype = ctypes.c_void_p
139+
140+ lib._netplan_iter_defs_per_devtype_free.argtypes = [ctypes.c_void_p]
141+ lib._netplan_iter_defs_per_devtype_free.restype = None
142+
143+ lib._netplan_netdef_id.argtypes = [ctypes.c_void_p]
144+ lib._netplan_netdef_id.restype = ctypes.c_char_p
145+
146+ lib.process_yaml_hierarchy(rootdir.encode('utf-8'))
147+ nds = list(_NetdefIdIterator(devtype))
148+ return [lib._netplan_netdef_id(nd).decode('utf-8') for nd in nds]
149+
150+
151 def get_generator_path():
152 return os.environ.get('NETPLAN_GENERATE_PATH', '/lib/netplan/generate')
153
154diff --git a/src/util.c b/src/util.c
155index a4c0dba..4c8d716 100644
156--- a/src/util.c
157+++ b/src/util.c
158@@ -327,3 +327,58 @@ get_global_network(int ip_family)
159 else
160 return "::/0";
161 }
162+
163+#define ENUM_FUNCTION(_radical, _type) _type netplan_ ## _radical ## _from_name(const char* val) \
164+{ \
165+ for (int i = 0; i < sizeof(netplan_ ## _radical ## _to_str); ++i) { \
166+ if (g_strcmp0(val, netplan_ ## _radical ## _to_str[i]) == 0) \
167+ return i; \
168+ } \
169+ return -1; \
170+}
171+
172+ENUM_FUNCTION(def_type, NetplanDefType);
173+
174+struct netdef_pertype_iterator {
175+ NetplanDefType type;
176+ GHashTableIter iter;
177+};
178+
179+struct netdef_pertype_iterator*
180+_netplan_iter_defs_per_devtype_init(const char *devtype)
181+{
182+ NetplanDefType type = netplan_def_type_from_name(devtype);
183+ struct netdef_pertype_iterator *iter = g_malloc0(sizeof(*iter));
184+ iter->type = type;
185+ if (netdefs)
186+ g_hash_table_iter_init(&iter->iter, netdefs);
187+ return iter;
188+}
189+
190+NetplanNetDefinition*
191+_netplan_iter_defs_per_devtype_next(struct netdef_pertype_iterator* it)
192+{
193+ gpointer key, value;
194+
195+ if (!netdefs)
196+ return NULL;
197+
198+ while (g_hash_table_iter_next(&it->iter, &key, &value)) {
199+ NetplanNetDefinition* netdef = value;
200+ if (netdef->type == it->type)
201+ return netdef;
202+ }
203+ return NULL;
204+}
205+
206+void
207+_netplan_iter_defs_per_devtype_free(struct netdef_pertype_iterator* it)
208+{
209+ g_free(it);
210+}
211+
212+const char*
213+_netplan_netdef_id(NetplanNetDefinition* nd)
214+{
215+ return nd->id;
216+}
217diff --git a/tests/test_cli_get_set.py b/tests/test_cli_get_set.py
218index 7a1799b..b5206e7 100644
219--- a/tests/test_cli_get_set.py
220+++ b/tests/test_cli_get_set.py
221@@ -31,13 +31,11 @@ from netplan.cli.core import Netplan
222 def _call_cli(args):
223 old_sys_argv = sys.argv
224 sys.argv = [old_sys_argv[0]] + args
225+ f = io.StringIO()
226 try:
227- f = io.StringIO()
228 with redirect_stdout(f):
229 Netplan().main()
230 return f.getvalue()
231- except Exception as e:
232- return e
233 finally:
234 sys.argv = old_sys_argv
235
236@@ -110,20 +108,20 @@ class TestSet(unittest.TestCase):
237 self.assertEquals('network:\n ethernets:\n eth0:\n dhcp4: true\n', f.read())
238
239 def test_set_empty_origin_hint(self):
240- err = self._set(['ethernets.eth0.dhcp4=true', '--origin-hint='])
241- self.assertIsInstance(err, Exception)
242- self.assertIn('Invalid/empty origin-hint', str(err))
243+ with self.assertRaises(Exception) as context:
244+ self._set(['ethernets.eth0.dhcp4=true', '--origin-hint='])
245+ self.assertTrue('Invalid/empty origin-hint' in str(context.exception))
246
247 def test_set_invalid(self):
248- err = self._set(['xxx.yyy=abc'])
249- self.assertIsInstance(err, Exception)
250- self.assertIn('unknown key \'xxx\'\n xxx:\n', str(err))
251+ with self.assertRaises(Exception) as context:
252+ self._set(['xxx.yyy=abc'])
253+ self.assertIn('unknown key \'xxx\'\n xxx:\n', str(context.exception))
254 self.assertFalse(os.path.isfile(self.path))
255
256 def test_set_invalid_validation(self):
257- err = self._set(['ethernets.eth0.set-name=myif0'])
258- self.assertIsInstance(err, Exception)
259- self.assertIn('eth0: \'set-name:\' requires \'match:\' properties', str(err))
260+ with self.assertRaises(Exception) as context:
261+ self._set(['ethernets.eth0.set-name=myif0'])
262+ self.assertIn('eth0: \'set-name:\' requires \'match:\' properties', str(context.exception))
263 self.assertFalse(os.path.isfile(self.path))
264
265 def test_set_invalid_validation2(self):
266@@ -134,9 +132,9 @@ class TestSet(unittest.TestCase):
267 mode: sit
268 local: 1.2.3.4
269 remote: 5.6.7.8''')
270- err = self._set(['tunnels.tun0.keys.input=12345'])
271- self.assertIsInstance(err, Exception)
272- self.assertIn('tun0: \'input-key\' is not required for this tunnel type', str(err))
273+ with self.assertRaises(Exception) as context:
274+ self._set(['tunnels.tun0.keys.input=12345'])
275+ self.assertIn('tun0: \'input-key\' is not required for this tunnel type', str(context.exception))
276
277 def test_set_append(self):
278 with open(self.path, 'w') as f:
279@@ -195,6 +193,20 @@ class TestSet(unittest.TestCase):
280 self.assertNotIn('addresses:', out)
281 self.assertNotIn('eth0:', out)
282
283+ def test_set_delete_subtree(self):
284+ with open(self.path, 'w') as f:
285+ f.write('''network:\n version: 2\n renderer: NetworkManager
286+ ethernets:
287+ eth0: {addresses: [1.2.3.4/24]}''')
288+ self._set(['network.ethernets=null'])
289+ self.assertTrue(os.path.isfile(self.path))
290+ with open(self.path, 'r') as f:
291+ out = f.read()
292+ self.assertIn('network:\n', out)
293+ self.assertIn(' version: 2\n', out)
294+ self.assertIn(' renderer: NetworkManager\n', out)
295+ self.assertNotIn('ethernets:', out)
296+
297 def test_set_delete_file(self):
298 with open(self.path, 'w') as f:
299 f.write('''network:
300@@ -220,9 +232,9 @@ class TestSet(unittest.TestCase):
301 f.write('''network:\n version: 2\n renderer: NetworkManager
302 ethernets:
303 eth0: {addresses: [1.2.3.4]}''')
304- err = self._set(['ethernets.eth0.addresses'])
305- self.assertIsInstance(err, Exception)
306- self.assertEquals('Invalid value specified', str(err))
307+ with self.assertRaises(Exception) as context:
308+ self._set(['ethernets.eth0.addresses'])
309+ self.assertEquals('Invalid value specified', str(context.exception))
310
311 def test_set_escaped_dot(self):
312 self._set([r'ethernets.eth0\.123.dhcp4=false'])
313@@ -231,9 +243,11 @@ class TestSet(unittest.TestCase):
314 self.assertIn('network:\n ethernets:\n eth0.123:\n dhcp4: false', f.read())
315
316 def test_set_invalid_input(self):
317- err = self._set([r'ethernets.eth0={dhcp4:false}'])
318- self.assertIsInstance(err, Exception)
319- self.assertEquals('Invalid input: {\'network\': {\'ethernets\': {\'eth0\': {\'dhcp4:false\': None}}}}', str(err))
320+ with self.assertRaises(Exception) as context:
321+ self._set([r'ethernets.eth0={dhcp4:false}'])
322+ self.assertEquals(
323+ 'Invalid input: {\'network\': {\'ethernets\': {\'eth0\': {\'dhcp4:false\': None}}}}',
324+ str(context.exception))
325
326 def test_set_override_existing_file(self):
327 override = os.path.join(self.workdir.name, 'etc', 'netplan', 'some-file.yaml')
328diff --git a/tests/test_utils.py b/tests/test_utils.py
329index 7954ec7..5c97ca2 100644
330--- a/tests/test_utils.py
331+++ b/tests/test_utils.py
332@@ -196,3 +196,36 @@ class TestUtils(unittest.TestCase):
333 remote: 0.0.0.0
334 key: 0.0.0.0''')
335 self.assertIsNone(utils.netplan_get_filename_by_id('some-id', self.workdir.name))
336+
337+ def test_netplan_get_ids_for_devtype(self):
338+ path = os.path.join(self.workdir.name, 'etc/netplan/a.yaml')
339+ with open(path, 'w') as f:
340+ f.write('''network:
341+ ethernets:
342+ id_b:
343+ dhcp4: true
344+ id_a:
345+ dhcp4: true
346+ vlans:
347+ en-intra:
348+ id: 3
349+ link: id_b
350+ dhcp4: true''')
351+ self.assertSetEqual(
352+ set(utils.netplan_get_ids_for_devtype("ethernets", self.workdir.name)),
353+ set(["id_a", "id_b"]))
354+
355+ def test_netplan_get_ids_for_devtype_no_dev(self):
356+ path = os.path.join(self.workdir.name, 'etc/netplan/a.yaml')
357+ with open(path, 'w') as f:
358+ f.write('''network:
359+ ethernets:
360+ id_b:
361+ dhcp4: true''')
362+ self.assertSetEqual(
363+ set(utils.netplan_get_ids_for_devtype("tunnels", self.workdir.name)),
364+ set([]))
365+
366+ def test_NetdefIdIterator_with_clear_netplan(self):
367+ utils.lib.netplan_clear_netdefs()
368+ self.assertSequenceEqual(list(utils._NetdefIdIterator("ethernets")), [])
diff --git a/debian/patches/series b/debian/patches/series
index 66b68b1..ba72f98 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -4,3 +4,5 @@
40003-Mute-gateway4-6-deprecation-warnings.patch40003-Mute-gateway4-6-deprecation-warnings.patch
5autopkgtest-fixes.patch5autopkgtest-fixes.patch
6nm-1.32.10-compat.patch6nm-1.32.10-compat.patch
70005-Implement-YAML-state-tracking-and-use-it-in-the-DBus.patch
80006-netplan-set-make-it-possible-to-unset-a-whole-devtyp.patch

Subscribers

People subscribed via source and target branches