Merge ~netplan-developers/netplan/+git/netplan-lp:cyphermox/forward-definition into ~netplan-developers/netplan/+git/netplan-lp:master

Proposed by Mathieu Trudel-Lapierre
Status: Merged
Merged at revision: e41215b24f43f5a67ba9f38bd6373a0f75405335
Proposed branch: ~netplan-developers/netplan/+git/netplan-lp:cyphermox/forward-definition
Merge into: ~netplan-developers/netplan/+git/netplan-lp:master
Diff against target: 586 lines (+389/-42)
4 files modified
src/parse.c (+116/-29)
src/parse.h (+6/-0)
tests/generate.py (+174/-13)
tests/integration.py (+93/-0)
Reviewer Review Type Date Requested Status
Tiago Stürmer Daitx (community) code review Approve
Review via email: mp+318939@code.launchpad.net

Description of the change

Add forward declaration support -- this means that the netplan parser needs to run multiple passes over a yaml file to first catch any identifiers that were "missing" (used before they were defined), then go through the yaml file again to piece everything together and take the steps that were skipped while the missing identifiers couldn't be matched to anything.

To post a comment you must log in.
Revision history for this message
Tiago Stürmer Daitx (tdaitx) :
review: Approve (code review)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/parse.c b/src/parse.c
2index 345c8ad..cac70f9 100644
3--- a/src/parse.c
4+++ b/src/parse.c
5@@ -50,6 +50,13 @@ GHashTable* netdefs;
6 * existing definition */
7 GHashTable* ids_in_file;
8
9+/* List of "seen" ids not found in netdefs yet by the parser.
10+ * These are removed when it exists in this list and we reach the point of
11+ * creating a netdef for that id; so by the time we're done parsing the yaml
12+ * document it should be empty. */
13+GHashTable *missing_id;
14+int missing_ids_found;
15+
16 /****************************************************
17 * Loading and error handling
18 ****************************************************/
19@@ -137,6 +144,22 @@ scalar(const yaml_node_t* node)
20 return (const char*) node->data.scalar.value;
21 }
22
23+static void
24+add_missing_node(const yaml_node_t* node)
25+{
26+ missing_node* missing;
27+
28+ /* Let's capture the current netdef we were playing with along with the
29+ * actual yaml_node_t that errors (that is an identifier not previously
30+ * seen by the compiler). We can use it later to write an sensible error
31+ * message and point the user in the right direction. */
32+ missing = g_new0(missing_node, 1);
33+ missing->netdef_id = cur_netdef->id;
34+ missing->node = node;
35+
36+ g_debug("recording missing yaml_node_t %s", scalar(node));
37+ g_hash_table_insert(missing_id, (gpointer)scalar(node), missing);
38+}
39
40 /**
41 * Check that node contains a valid ID/interface name. Raise GError if not.
42@@ -276,11 +299,11 @@ handle_netdef_id_ref(yaml_document_t* doc, yaml_node_t* node, const void* data,
43 net_definition* ref = NULL;
44
45 ref = g_hash_table_lookup(netdefs, scalar(node));
46- if (!ref)
47- return yaml_error(node, error, "%s: interface %s is not defined",
48- cur_netdef->id, scalar(node));
49-
50- *((net_definition**) ((void*) cur_netdef + offset)) = ref;
51+ if (!ref) {
52+ add_missing_node(node);
53+ } else {
54+ *((net_definition**) ((void*) cur_netdef + offset)) = ref;
55+ }
56 return TRUE;
57 }
58
59@@ -563,14 +586,15 @@ handle_interfaces(yaml_document_t* doc, yaml_node_t* node, const void* data, GEr
60
61 assert_type(entry, YAML_SCALAR_NODE);
62 component = g_hash_table_lookup(netdefs, scalar(entry));
63- if (!component)
64- return yaml_error(node, error, "%s: interface %s is not defined",
65- cur_netdef->id, scalar(entry));
66- component_ref_ptr = ((char**) ((void*) component + GPOINTER_TO_UINT(data)));
67- if (*component_ref_ptr)
68- return yaml_error(node, error, "%s: interface %s is already assigned to %s",
69- cur_netdef->id, scalar(entry), *component_ref_ptr);
70- *component_ref_ptr = cur_netdef->id;
71+ if (!component) {
72+ add_missing_node(entry);
73+ } else {
74+ component_ref_ptr = ((char**) ((void*) component + GPOINTER_TO_UINT(data)));
75+ if (*component_ref_ptr && *component_ref_ptr != cur_netdef->id)
76+ return yaml_error(node, error, "%s: interface %s is already assigned to %s",
77+ cur_netdef->id, scalar(entry), *component_ref_ptr);
78+ *component_ref_ptr = cur_netdef->id;
79+ }
80 }
81
82 return TRUE;
83@@ -720,22 +744,22 @@ handle_bridge_path_cost(yaml_document_t* doc, yaml_node_t* node, const void* dat
84 assert_type(value, YAML_SCALAR_NODE);
85
86 component = g_hash_table_lookup(netdefs, scalar(key));
87- if (!component)
88- return yaml_error(node, error, "%s: interface %s is not defined",
89- cur_netdef->id, scalar(key));
90-
91- ref_ptr = ((guint*) ((void*) component + GPOINTER_TO_UINT(data)));
92- if (*ref_ptr)
93- return yaml_error(node, error, "%s: interface %s already has a path cost of %u",
94- cur_netdef->id, scalar(key), *ref_ptr);
95+ if (!component) {
96+ add_missing_node(key);
97+ } else {
98+ ref_ptr = ((guint*) ((void*) component + GPOINTER_TO_UINT(data)));
99+ if (*ref_ptr)
100+ return yaml_error(node, error, "%s: interface %s already has a path cost of %u",
101+ cur_netdef->id, scalar(key), *ref_ptr);
102
103- v = g_ascii_strtoull(scalar(value), &endptr, 10);
104- if (*endptr != '\0' || v > G_MAXUINT)
105- return yaml_error(node, error, "invalid unsigned int value %s", scalar(value));
106+ v = g_ascii_strtoull(scalar(value), &endptr, 10);
107+ if (*endptr != '\0' || v > G_MAXUINT)
108+ return yaml_error(node, error, "invalid unsigned int value %s", scalar(value));
109
110- g_debug("%s: adding path '%s' of cost: %d", cur_netdef->id, scalar(key), v);
111+ g_debug("%s: adding path '%s' of cost: %d", cur_netdef->id, scalar(key), v);
112
113- *ref_ptr = v;
114+ *ref_ptr = v;
115+ }
116 }
117 return TRUE;
118 }
119@@ -959,8 +983,15 @@ handle_network_renderer(yaml_document_t* doc, yaml_node_t* node, const void* _,
120 static gboolean
121 validate_netdef(net_definition* nd, yaml_node_t* node, GError** error)
122 {
123+ int missing_id_count = g_hash_table_size(missing_id);
124 g_assert(nd->type != ND_NONE);
125
126+ /* Skip all validation if we're missing some definition IDs (devices).
127+ * The ones we have yet to see may be necessary for validation to succeed,
128+ * we can complete it on the next parser pass. */
129+ if (missing_id_count > 0)
130+ return TRUE;
131+
132 /* set-name: requires match: */
133 if (nd->set_name && !nd->has_match)
134 return yaml_error(node, error, "%s: set-name: requires match: properties", nd->id);
135@@ -1010,6 +1041,12 @@ handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, G
136
137 assert_type(value, YAML_MAPPING_NODE);
138
139+ /* At this point we've seen a new starting definition, if it has been
140+ * already mentioned in another netdef, removing it from our "missing"
141+ * list. */
142+ if(g_hash_table_remove(missing_id, scalar(key)))
143+ missing_ids_found++;
144+
145 cur_netdef = g_hash_table_lookup(netdefs, scalar(key));
146 if (cur_netdef) {
147 /* already exists, overriding/amending previous definition */
148@@ -1026,8 +1063,9 @@ handle_network_type(yaml_document_t* doc, yaml_node_t* node, const void* data, G
149 g_hash_table_insert(netdefs, cur_netdef->id, cur_netdef);
150 }
151
152- if (!g_hash_table_add(ids_in_file, cur_netdef->id))
153- return yaml_error(key, error, "Duplicate net definition ID '%s'", cur_netdef->id);
154+ // XXX: breaks multi-pass parsing.
155+ //if (!g_hash_table_add(ids_in_file, cur_netdef->id))
156+ // return yaml_error(key, error, "Duplicate net definition ID '%s'", cur_netdef->id);
157
158 /* and fill it with definitions */
159 switch (cur_netdef->type) {
160@@ -1074,7 +1112,55 @@ const mapping_entry_handler root_handlers[] = {
161 {NULL}
162 };
163
164+/**
165+ * Handle multiple-pass parsing of the yaml document.
166+ */
167+static gboolean
168+process_document(yaml_document_t* doc, GError** error)
169+{
170+ gboolean ret;
171+ int previously_found;
172+ int still_missing;
173+
174+ g_assert(missing_id == NULL);
175+ missing_id = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
176+
177+ do {
178+ g_debug("starting new processing pass");
179+
180+ previously_found = missing_ids_found;
181+ missing_ids_found = 0;
182+
183+ ret = process_mapping(doc, yaml_document_get_root_node(doc), root_handlers, error);
184+
185+ still_missing = g_hash_table_size(missing_id);
186+
187+ if (still_missing > 0 && missing_ids_found == previously_found)
188+ break;
189+ } while (still_missing > 0 || missing_ids_found > 0);
190
191+ if (g_hash_table_size(missing_id) > 0) {
192+ GHashTableIter iter;
193+ gpointer key, value;
194+ missing_node *missing;
195+
196+ g_clear_error(error);
197+
198+ /* Get the first missing identifier we can get from our list, to
199+ * approximate early failure and give the user a meaningful error. */
200+ g_hash_table_iter_init (&iter, missing_id);
201+ g_hash_table_iter_next (&iter, &key, &value);
202+ missing = (missing_node*) value;
203+
204+ return yaml_error(missing->node, error, "%s: interface %s is not defined",
205+ missing->netdef_id,
206+ key);
207+ }
208+
209+ g_hash_table_destroy(missing_id);
210+ missing_id = NULL;
211+ return ret;
212+}
213
214 /**
215 * Parse given YAML file and create/update global "netdefs" list.
216@@ -1098,7 +1184,8 @@ parse_yaml(const char* filename, GError** error)
217 g_assert(ids_in_file == NULL);
218 ids_in_file = g_hash_table_new(g_str_hash, NULL);
219
220- ret = process_mapping(&doc, yaml_document_get_root_node(&doc), root_handlers, error);
221+ ret = process_document(&doc, error);
222+
223 cur_netdef = NULL;
224 yaml_document_delete(&doc);
225 g_hash_table_destroy(ids_in_file);
226diff --git a/src/parse.h b/src/parse.h
227index 75d6531..f2e9c81 100644
228--- a/src/parse.h
229+++ b/src/parse.h
230@@ -18,6 +18,7 @@
231 #pragma once
232
233 #include <uuid.h>
234+#include <yaml.h>
235
236 /****************************************************
237 * Parsed definitions
238@@ -41,6 +42,11 @@ typedef enum {
239 BACKEND_NM,
240 } netdef_backend;
241
242+typedef struct missing_node {
243+ char* netdef_id;
244+ yaml_node_t* node;
245+} missing_node;
246+
247 /**
248 * Represent a configuration stanza
249 */
250diff --git a/tests/generate.py b/tests/generate.py
251index e168283..179b4de 100755
252--- a/tests/generate.py
253+++ b/tests/generate.py
254@@ -777,6 +777,27 @@ Address=1.2.3.4/12
255 unmanaged-devices+=interface-name:br0,''')
256 self.assert_udev(None)
257
258+ def test_bridge_forward_declaration(self):
259+ self.generate('''network:
260+ version: 2
261+ bridges:
262+ br0:
263+ interfaces: [eno1, switchports]
264+ dhcp4: true
265+ ethernets:
266+ eno1: {}
267+ switchports:
268+ match:
269+ driver: yayroute
270+''')
271+
272+ self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n',
273+ 'br0.network': ND_DHCP4 % 'br0',
274+ 'eno1.network': '[Match]\nName=eno1\n\n'
275+ '[Network]\nBridge=br0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n',
276+ 'switchports.network': '[Match]\nDriver=yayroute\n\n'
277+ '[Network]\nBridge=br0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n'})
278+
279 def test_bridge_components(self):
280 self.generate('''network:
281 version: 2
282@@ -1836,6 +1857,58 @@ address1=1.2.3.4/12
283 self.assert_networkd({})
284 self.assert_udev(None)
285
286+ def test_bridge_forward_declaration(self):
287+ self.generate('''network:
288+ version: 2
289+ renderer: NetworkManager
290+ bridges:
291+ br0:
292+ interfaces: [eno1, switchport]
293+ dhcp4: true
294+ ethernets:
295+ eno1: {}
296+ switchport:
297+ match:
298+ name: enp2s1
299+''')
300+
301+ self.assert_nm({'eno1': '''[connection]
302+id=netplan-eno1
303+type=ethernet
304+interface-name=eno1
305+slave-type=bridge
306+master=br0
307+
308+[ethernet]
309+wake-on-lan=0
310+
311+[ipv4]
312+method=link-local
313+''',
314+ 'switchport': '''[connection]
315+id=netplan-switchport
316+type=ethernet
317+interface-name=enp2s1
318+slave-type=bridge
319+master=br0
320+
321+[ethernet]
322+wake-on-lan=0
323+
324+[ipv4]
325+method=link-local
326+''',
327+ 'br0': '''[connection]
328+id=netplan-br0
329+type=bridge
330+interface-name=br0
331+
332+[ipv4]
333+method=auto
334+'''})
335+ self.assert_networkd({})
336+ self.assert_udev(None)
337+
338 def test_bridge_components(self):
339 self.generate('''network:
340 version: 2
341@@ -2384,17 +2457,6 @@ class TestConfigErrors(TestBase):
342 err = self.generate('network:\n version: 1', expect_fail=True)
343 self.assertIn('/a.yaml line 1 column 11: Only version 2 is supported', err)
344
345- def test_duplicate_id(self):
346- err = self.generate('''network:
347- version: 2
348- ethernets:
349- id0:
350- wakeonlan: true
351- id0:
352- wakeonlan: true
353-''', expect_fail=True)
354- self.assertIn("Duplicate net definition ID 'id0'", err)
355-
356 def test_id_redef_type_mismatch(self):
357 err = self.generate('''network:
358 version: 2
359@@ -2448,7 +2510,7 @@ class TestConfigErrors(TestBase):
360 bridges:
361 br0:
362 interfaces: ['foo']''', expect_fail=True)
363- self.assertIn('/a.yaml line 4 column 18: br0: interface foo is not defined\n', err)
364+ self.assertIn('/a.yaml line 4 column 19: br0: interface foo is not defined\n', err)
365
366 def test_bridge_multiple_assignments(self):
367 err = self.generate('''network:
368@@ -2717,7 +2779,7 @@ class TestConfigErrors(TestBase):
369 version: 2
370 vlans:
371 ena: {id: 1, link: en1}''', expect_fail=True)
372- self.assertIn('interface en1 is not defined\n', err)
373+ self.assertIn('ena: interface en1 is not defined\n', err)
374
375 def test_device_bad_route_to(self):
376 self.generate('''network:
377@@ -2870,6 +2932,105 @@ class TestConfigErrors(TestBase):
378 dhcp4: true''', expect_fail=True)
379
380
381+class TestForwardDeclaration(TestBase):
382+
383+ def test_fwdecl_bridge_on_bond(self):
384+ self.generate('''network:
385+ version: 2
386+ bridges:
387+ br0:
388+ interfaces: ['bond0']
389+ dhcp4: true
390+ bonds:
391+ bond0:
392+ interfaces: ['eth0', 'eth1']
393+ ethernets:
394+ eth0:
395+ match:
396+ macaddress: 00:01:02:03:04:05
397+ set-name: eth0
398+ eth1:
399+ match:
400+ macaddress: 02:01:02:03:04:05
401+ set-name: eth1
402+''')
403+
404+ self.assert_networkd({'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n',
405+ 'br0.network': ND_DHCP4 % 'br0',
406+ 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n',
407+ 'bond0.network': '[Match]\nName=bond0\n\n'
408+ '[Network]\nBridge=br0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n',
409+ 'eth0.link': '[Match]\nMACAddress=00:01:02:03:04:05\n\n'
410+ '[Link]\nName=eth0\nWakeOnLan=off\n',
411+ 'eth0.network': '[Match]\nMACAddress=00:01:02:03:04:05\nName=eth0\n\n'
412+ '[Network]\nBond=bond0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n',
413+ 'eth1.link': '[Match]\nMACAddress=02:01:02:03:04:05\n\n'
414+ '[Link]\nName=eth1\nWakeOnLan=off\n',
415+ 'eth1.network': '[Match]\nMACAddress=02:01:02:03:04:05\nName=eth1\n\n'
416+ '[Network]\nBond=bond0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n'})
417+
418+ def test_fwdecl_feature_blend(self):
419+ self.generate('''network:
420+ version: 2
421+ vlans:
422+ vlan1:
423+ link: 'br0'
424+ id: 1
425+ dhcp4: true
426+ bridges:
427+ br0:
428+ interfaces: ['bond0', 'eth2']
429+ parameters:
430+ path-cost:
431+ eth2: 1000
432+ bond0: 8888
433+ bonds:
434+ bond0:
435+ interfaces: ['eth0', 'br1']
436+ ethernets:
437+ eth0:
438+ match:
439+ macaddress: 00:01:02:03:04:05
440+ set-name: eth0
441+ bridges:
442+ br1:
443+ interfaces: ['eth1']
444+ ethernets:
445+ eth1:
446+ match:
447+ macaddress: 02:01:02:03:04:05
448+ set-name: eth1
449+ eth2:
450+ match:
451+ name: eth2
452+''')
453+
454+ self.assert_networkd({'vlan1.netdev': '[NetDev]\nName=vlan1\nKind=vlan\n\n'
455+ '[VLAN]\nId=1\n',
456+ 'vlan1.network': ND_DHCP4 % 'vlan1',
457+ 'br0.netdev': '[NetDev]\nName=br0\nKind=bridge\n',
458+ 'br0.network': '[Match]\nName=br0\n\n'
459+ '[Network]\nVLAN=vlan1\n',
460+ 'bond0.netdev': '[NetDev]\nName=bond0\nKind=bond\n',
461+ 'bond0.network': '[Match]\nName=bond0\n\n'
462+ '[Network]\nBridge=br0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n\n'
463+ '[Bridge]\nCost=8888\n',
464+ 'eth2.network': '[Match]\nName=eth2\n\n'
465+ '[Network]\nBridge=br0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n\n'
466+ '[Bridge]\nCost=1000\n',
467+ 'br1.netdev': '[NetDev]\nName=br1\nKind=bridge\n',
468+ 'br1.network': '[Match]\nName=br1\n\n'
469+ '[Network]\nBond=bond0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n',
470+ 'eth0.link': '[Match]\nMACAddress=00:01:02:03:04:05\n\n'
471+ '[Link]\nName=eth0\nWakeOnLan=off\n',
472+ 'eth0.network': '[Match]\nMACAddress=00:01:02:03:04:05\nName=eth0\n\n'
473+ '[Network]\nBond=bond0\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n',
474+ 'eth1.link': '[Match]\nMACAddress=02:01:02:03:04:05\n\n'
475+ '[Link]\nName=eth1\nWakeOnLan=off\n',
476+ 'eth1.network': '[Match]\nMACAddress=02:01:02:03:04:05\nName=eth1\n\n'
477+ '[Network]\nBridge=br1\nLinkLocalAddressing=no\nIPv6AcceptRA=no\n'})
478+
479+
480 class TestMerging(TestBase):
481 '''multiple *.yaml merging'''
482
483diff --git a/tests/integration.py b/tests/integration.py
484index b1bfbcc..6fe97f6 100755
485--- a/tests/integration.py
486+++ b/tests/integration.py
487@@ -1147,6 +1147,99 @@ wpa_passphrase=12345678
488 universal_newlines=True)
489 self.assertRegex(out, 'DNS.*192.168.5.1')
490
491+ def test_mix_bridge_on_bond(self):
492+ self.setup_eth(None)
493+ self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL)
494+ self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL)
495+ self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1'], stderr=subprocess.DEVNULL)
496+ with open(self.config, 'w') as f:
497+ f.write('''network:
498+ renderer: %(r)s
499+ bridges:
500+ br0:
501+ interfaces: [bond0]
502+ dhcp4: true
503+ bonds:
504+ bond0:
505+ interfaces: [ethbn, ethb2]
506+ parameters:
507+ mii-monitor-interval: 100000
508+ ethernets:
509+ ethbn:
510+ match: {name: %(ec)s}
511+ ethb2:
512+ match: {name: %(e2c)s}
513+''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client})
514+ self.generate_and_settle()
515+ self.assert_iface_up(self.dev_e_client,
516+ ['master bond0'],
517+ ['inet '])
518+ self.assert_iface_up(self.dev_e2_client,
519+ ['master bond0'],
520+ ['inet '])
521+ self.assert_iface_up('bond0',
522+ ['master br0'])
523+ self.assert_iface_up('br0',
524+ ['inet 192.168'])
525+ with open('/sys/class/net/bond0/bonding/slaves') as f:
526+ result = f.read().strip()
527+ self.assertIn(self.dev_e_client, result)
528+ self.assertIn(self.dev_e2_client, result)
529+
530+ def test_mix_vlan_on_bridge_on_bond(self):
531+ self.setup_eth(None)
532+ self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'bond0'], stderr=subprocess.DEVNULL)
533+ self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br0'], stderr=subprocess.DEVNULL)
534+ self.addCleanup(subprocess.call, ['ip', 'link', 'delete', 'br1'], stderr=subprocess.DEVNULL)
535+ with open(self.config, 'w') as f:
536+ f.write('''network:
537+ renderer: %(r)s
538+ version: 2
539+ vlans:
540+ vlan1:
541+ link: 'br0'
542+ id: 1
543+ addresses: [ '10.10.10.1/24' ]
544+ bridges:
545+ br0:
546+ interfaces: ['bond0', 'vlan2']
547+ parameters:
548+ path-cost:
549+ bond0: 1000
550+ vlan2: 2000
551+ bonds:
552+ bond0:
553+ interfaces: ['br1']
554+ parameters:
555+ mii-monitor-interval: 100000
556+ bridges:
557+ br1:
558+ interfaces: ['ethb2']
559+ vlans:
560+ vlan2:
561+ link: ethbn
562+ id: 2
563+ ethernets:
564+ ethbn:
565+ match: {name: %(ec)s}
566+ ethb2:
567+ match: {name: %(e2c)s}
568+''' % {'r': self.backend, 'ec': self.dev_e_client, 'e2c': self.dev_e2_client})
569+ self.generate_and_settle()
570+ self.assert_iface_up('vlan1', ['vlan1@br0'])
571+ self.assert_iface_up('vlan2',
572+ ['vlan2@' + self.dev_e_client, 'master br0'])
573+ self.assert_iface_up(self.dev_e2_client,
574+ ['master br1'],
575+ ['inet '])
576+ self.assert_iface_up('br1',
577+ ['master bond0'])
578+ self.assert_iface_up('bond0',
579+ ['master br0'])
580+ with open('/sys/class/net/bond0/bonding/slaves') as f:
581+ result = f.read().strip()
582+ self.assertIn('br1', result)
583+
584
585 class TestNetworkd(NetworkTestBase, _CommonTests):
586 backend = 'networkd'

Subscribers

People subscribed via source and target branches

to all changes: