Merge netplan:cyphermox/routes into netplan:master

Proposed by Mathieu Trudel-Lapierre on 2016-12-07
Status: Merged
Approved by: Mathieu Trudel-Lapierre on 2016-12-14
Approved revision: 88e5179fcd84a69e9b65c3f4f8a550f7d82eb555
Merged at revision: 8498f9c72f33c26ce2e8b42b45d71b2a89594907
Proposed branch: netplan:cyphermox/routes
Merge into: netplan:master
Diff against target: 814 lines (+643/-0)
6 files modified
src/networkd.c (+9/-0)
src/nm.c (+25/-0)
src/parse.c (+124/-0)
src/parse.h (+13/-0)
tests/generate.py (+399/-0)
tests/integration.py (+73/-0)
Reviewer Review Type Date Requested Status
Mathieu Trudel-Lapierre Approve on 2016-12-14
Martin Pitt 2016-12-07 Needs Fixing on 2016-12-13
Review via email: mp+312683@code.launchpad.net

Description of the Change

Add support for defining routes.

To post a comment you must log in.
Martin Pitt (pitti) wrote :

The general structure looks good to me, I have a bunch of inline comments.

This is missing the NM implementation and extending the integration test case to cover this.

review: Needs Fixing
Martin Pitt (pitti) :
Martin Pitt (pitti) wrote :

Very close now, thanks! A bunch of nitpicks, two errors, and some missing tests. Feel free to land this yourself after this round, as I'll be on EOY holidays from tomorrow on.

review: Needs Fixing
Mathieu Trudel-Lapierre (cyphermox) wrote :

Marking as approved after Martin's review points have been addressed.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/networkd.c b/src/networkd.c
2index 2d0e586..86bb51f 100644
3--- a/src/networkd.c
4+++ b/src/networkd.c
5@@ -168,6 +168,15 @@ write_network_file(net_definition* def, const char* rootdir, const char* path)
6 if (nd->vlan_link == def)
7 g_string_append_printf(s, "VLAN=%s\n", nd->id);
8 }
9+ if (def->routes != NULL) {
10+ for (unsigned i = 0; i < def->routes->len; ++i) {
11+ ip_route* cur_route = g_array_index (def->routes, ip_route*, i);
12+ g_string_append_printf(s, "\n[Route]\nDestination=%s\nGateway=%s\n",
13+ cur_route->to, cur_route->via);
14+ if (cur_route->metric != METRIC_UNSPEC)
15+ g_string_append_printf(s, "Metric=%d\n", cur_route->metric);
16+ }
17+ }
18
19 /* NetworkManager compatible route metrics */
20 if (def->dhcp4 || def->dhcp6)
21diff --git a/src/nm.c b/src/nm.c
22index f1d39af..bb87dae 100644
23--- a/src/nm.c
24+++ b/src/nm.c
25@@ -19,6 +19,7 @@
26 #include <string.h>
27 #include <unistd.h>
28 #include <sys/stat.h>
29+#include <arpa/inet.h>
30
31 #include <glib.h>
32 #include <glib/gprintf.h>
33@@ -116,6 +117,26 @@ write_search_domains(const net_definition* def, GString *s)
34 }
35 }
36
37+static void
38+write_routes(const net_definition* def, GString *s, int family)
39+{
40+ if (def->routes != NULL) {
41+ for (unsigned i = 0, j = 1; i < def->routes->len; ++i) {
42+ ip_route *cur_route = g_array_index(def->routes, ip_route*, i);
43+
44+ if (cur_route->family != family)
45+ continue;
46+
47+ g_string_append_printf(s, "route%d=%s,%s",
48+ j, cur_route->to, cur_route->via);
49+ if (cur_route->metric != METRIC_UNSPEC)
50+ g_string_append_printf(s, ",%d", cur_route->metric);
51+ g_string_append(s, "\n");
52+ j++;
53+ }
54+ }
55+}
56+
57 /**
58 * Generate NetworkManager configuration in @rootdir/run/NetworkManager/ for a
59 * particular net_definition and wifi_access_point, as NM requires a separate
60@@ -235,6 +256,7 @@ write_nm_conf_access_point(net_definition* def, const char* rootdir, const wifi_
61 g_string_append(s, "\n");
62 }
63 write_search_domains(def, s);
64+ write_routes(def, s, AF_INET);
65
66 if (def->dhcp6 || def->ip6_addresses || def->gateway6 || def->ip6_nameservers) {
67 g_string_append(s, "\n[ipv6]\n");
68@@ -253,6 +275,9 @@ write_nm_conf_access_point(net_definition* def, const char* rootdir, const wifi_
69 /* nm-settings(5) specifies search-domain for both [ipv4] and [ipv6] --
70 * do we really need to repeat it here? */
71 write_search_domains(def, s);
72+
73+ /* We can only write valid routes if there is a DHCPv6 or static IPv6 address */
74+ write_routes(def, s, AF_INET6);
75 }
76
77 conf_path = g_strjoin(NULL, "run/NetworkManager/system-connections/netplan-", def->id, NULL);
78diff --git a/src/parse.c b/src/parse.c
79index 2db2b11..32f3981 100644
80--- a/src/parse.c
81+++ b/src/parse.c
82@@ -29,6 +29,7 @@
83
84 /* convenience macro to put the offset of a net_definition field into "void* data" */
85 #define netdef_offset(field) GUINT_TO_POINTER(offsetof(net_definition, field))
86+#define route_offset(field) GUINT_TO_POINTER(offsetof(ip_route, field))
87
88 /* file that is currently being processed, for useful error messages */
89 const char* current_file;
90@@ -38,6 +39,8 @@ net_definition* cur_netdef;
91 /* wifi AP that is currently being processed */
92 wifi_access_point* cur_access_point;
93
94+ip_route* cur_route;
95+
96 netdef_backend backend_global, backend_cur_type;
97
98 /* Global ID → net_definition* map for all parsed config files */
99@@ -625,6 +628,122 @@ handle_nameservers_addresses(yaml_document_t* doc, yaml_node_t* node, const void
100 return TRUE;
101 }
102
103+
104+static int
105+get_ip_family(const char* address)
106+{
107+ struct in_addr a4;
108+ struct in6_addr a6;
109+ g_autofree char *ip_str;
110+ char *prefix_len;
111+ int ret = -1;
112+
113+ ip_str = g_strdup(address);
114+ prefix_len = strrchr(ip_str, '/');
115+ if (prefix_len)
116+ *prefix_len = '\0';
117+
118+ ret = inet_pton(AF_INET, ip_str, &a4);
119+ g_assert(ret >= 0);
120+ if (ret > 0)
121+ return AF_INET;
122+
123+ ret = inet_pton(AF_INET6, ip_str, &a6);
124+ g_assert(ret >= 0);
125+ if (ret > 0)
126+ return AF_INET6;
127+
128+ return -1;
129+}
130+
131+static gboolean
132+check_and_set_family(int family)
133+{
134+ if (cur_route->family != -1 && cur_route->family != family)
135+ return FALSE;
136+
137+ cur_route->family = family;
138+
139+ return TRUE;
140+}
141+
142+static gboolean
143+handle_routes_ip(yaml_document_t* doc, yaml_node_t* node, const void* data, GError** error)
144+{
145+ guint offset = GPOINTER_TO_UINT(data);
146+ int family = get_ip_family(scalar(node));
147+ char** dest = (char**) ((void*) cur_route + offset);
148+ g_free(*dest);
149+
150+ if (family < 0)
151+ return yaml_error(node, error, "invalid IP family %d", family);
152+
153+ if (!check_and_set_family(family))
154+ return yaml_error(node, error, "IP family mismatch in route to %s", scalar(node));
155+
156+ *dest = g_strdup(scalar(node));
157+
158+ return TRUE;
159+}
160+
161+static gboolean
162+handle_routes_metric(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
163+{
164+ guint64 v;
165+ gchar* endptr;
166+
167+ v = g_ascii_strtoull(scalar(node), &endptr, 10);
168+ if (*endptr != '\0' || v > G_MAXUINT)
169+ return yaml_error(node, error, "invalid unsigned int value %s", scalar(node));
170+
171+ cur_route->metric = (guint) v;
172+ return TRUE;
173+}
174+
175+/****************************************************
176+ * Grammar and handlers for network config "routes" entry
177+ ****************************************************/
178+
179+const mapping_entry_handler routes_handlers[] = {
180+ {"to", YAML_SCALAR_NODE, handle_routes_ip, NULL, route_offset(to)},
181+ {"via", YAML_SCALAR_NODE, handle_routes_ip, NULL, route_offset(via)},
182+ {"metric", YAML_SCALAR_NODE, handle_routes_metric},
183+ {NULL}
184+};
185+
186+static gboolean
187+handle_routes(yaml_document_t* doc, yaml_node_t* node, const void* _, GError** error)
188+{
189+ for (yaml_node_item_t *i = node->data.sequence.items.start; i < node->data.sequence.items.top; i++) {
190+ yaml_node_t *entry = yaml_document_get_node(doc, *i);
191+
192+ cur_route = g_new0(ip_route, 1);
193+ cur_route->family = G_MAXUINT; /* 0 is a valid family ID */
194+ cur_route->metric = G_MAXUINT; /* 0 is a valid metric */
195+
196+ if (process_mapping(doc, entry, routes_handlers, error)) {
197+ if (!cur_netdef->routes) {
198+ cur_netdef->routes = g_array_new(FALSE, FALSE, sizeof(ip_route*));
199+ }
200+
201+ g_array_append_val(cur_netdef->routes, cur_route);
202+ }
203+
204+ if (!cur_route->to || !cur_route->via)
205+ return yaml_error(node, error, "route must include both a 'to' and 'via' IP");
206+
207+ cur_route = NULL;
208+
209+ if (error && *error)
210+ return FALSE;
211+ }
212+ return TRUE;
213+}
214+
215+/****************************************************
216+ * Grammar and handlers for network devices
217+ ****************************************************/
218+
219 const mapping_entry_handler nameservers_handlers[] = {
220 {"search", YAML_SEQUENCE_NODE, handle_nameservers_search},
221 {"addresses", YAML_SEQUENCE_NODE, handle_nameservers_addresses},
222@@ -642,6 +761,7 @@ const mapping_entry_handler ethernet_def_handlers[] = {
223 {"gateway4", YAML_SCALAR_NODE, handle_gateway4},
224 {"gateway6", YAML_SCALAR_NODE, handle_gateway6},
225 {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers},
226+ {"routes", YAML_SEQUENCE_NODE, handle_routes},
227 {NULL}
228 };
229
230@@ -657,6 +777,7 @@ const mapping_entry_handler wifi_def_handlers[] = {
231 {"gateway6", YAML_SCALAR_NODE, handle_gateway6},
232 {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers},
233 {"access-points", YAML_MAPPING_NODE, handle_wifi_access_points},
234+ {"routes", YAML_SEQUENCE_NODE, handle_routes},
235 {NULL}
236 };
237
238@@ -669,6 +790,7 @@ const mapping_entry_handler bridge_def_handlers[] = {
239 {"gateway6", YAML_SCALAR_NODE, handle_gateway6},
240 {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers},
241 {"interfaces", YAML_SEQUENCE_NODE, handle_interfaces, NULL, netdef_offset(bridge)},
242+ {"routes", YAML_SEQUENCE_NODE, handle_routes},
243 {NULL}
244 };
245
246@@ -681,6 +803,7 @@ const mapping_entry_handler bond_def_handlers[] = {
247 {"gateway6", YAML_SCALAR_NODE, handle_gateway6},
248 {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers},
249 {"interfaces", YAML_SEQUENCE_NODE, handle_interfaces, NULL, netdef_offset(bond)},
250+ {"routes", YAML_SEQUENCE_NODE, handle_routes},
251 {NULL}
252 };
253
254@@ -694,6 +817,7 @@ const mapping_entry_handler vlan_def_handlers[] = {
255 {"nameservers", YAML_MAPPING_NODE, NULL, nameservers_handlers},
256 {"id", YAML_SCALAR_NODE, handle_netdef_guint, NULL, netdef_offset(vlan_id)},
257 {"link", YAML_SCALAR_NODE, handle_netdef_id_ref, NULL, netdef_offset(vlan_link)},
258+ {"routes", YAML_SEQUENCE_NODE, handle_routes},
259 {NULL}
260 };
261
262diff --git a/src/parse.h b/src/parse.h
263index 7c5bf2b..98d7d1c 100644
264--- a/src/parse.h
265+++ b/src/parse.h
266@@ -61,6 +61,7 @@ typedef struct net_definition {
267 GArray* ip4_nameservers;
268 GArray* ip6_nameservers;
269 GArray* search_domains;
270+ GArray* routes;
271
272 /* master ID for slave devices */
273 char* bridge;
274@@ -97,6 +98,18 @@ typedef struct {
275 char* password;
276 } wifi_access_point;
277
278+#define METRIC_UNSPEC G_MAXUINT
279+
280+typedef struct {
281+ guint family;
282+
283+ char* to;
284+ char* via;
285+
286+ /* valid metrics are valid positive integers.
287+ * invalid metrics are represented by METRIC_UNSPEC */
288+ guint metric;
289+} ip_route;
290
291 /* Written/updated by parse_yaml(): char* id → net_definition */
292 extern GHashTable* netdefs;
293diff --git a/tests/generate.py b/tests/generate.py
294index b3a127b..7db0a03 100755
295--- a/tests/generate.py
296+++ b/tests/generate.py
297@@ -510,6 +510,118 @@ Address=2001:FFfe::1/64
298 RouteMetric=100
299 '''})
300
301+ def test_route_v4_single(self):
302+ self.generate('''network:
303+ version: 2
304+ ethernets:
305+ engreen:
306+ addresses: ["192.168.14.2/24"]
307+ routes:
308+ - to: 10.10.10.0/24
309+ via: 192.168.14.20
310+ metric: 100
311+ ''')
312+
313+ self.assert_networkd({'engreen.network': '''[Match]
314+Name=engreen
315+
316+[Network]
317+Address=192.168.14.2/24
318+
319+[Route]
320+Destination=10.10.10.0/24
321+Gateway=192.168.14.20
322+Metric=100
323+'''})
324+
325+ def test_route_v4_multiple(self):
326+ self.generate('''network:
327+ version: 2
328+ ethernets:
329+ engreen:
330+ addresses: ["192.168.14.2/24"]
331+ routes:
332+ - to: 8.8.0.0/16
333+ via: 192.168.1.1
334+ - to: 10.10.10.8
335+ via: 192.168.1.2
336+ metric: 5000
337+ - to: 11.11.11.0/24
338+ via: 192.168.1.3
339+ metric: 9999
340+ ''')
341+
342+ self.assert_networkd({'engreen.network': '''[Match]
343+Name=engreen
344+
345+[Network]
346+Address=192.168.14.2/24
347+
348+[Route]
349+Destination=8.8.0.0/16
350+Gateway=192.168.1.1
351+
352+[Route]
353+Destination=10.10.10.8
354+Gateway=192.168.1.2
355+Metric=5000
356+
357+[Route]
358+Destination=11.11.11.0/24
359+Gateway=192.168.1.3
360+Metric=9999
361+'''})
362+
363+ def test_route_v6_single(self):
364+ self.generate('''network:
365+ version: 2
366+ ethernets:
367+ enblue:
368+ addresses: ["192.168.1.3/24"]
369+ routes:
370+ - to: 2001:dead:beef::2/64
371+ via: 2001:beef:beef::1''')
372+
373+ self.assert_networkd({'enblue.network': '''[Match]
374+Name=enblue
375+
376+[Network]
377+Address=192.168.1.3/24
378+
379+[Route]
380+Destination=2001:dead:beef::2/64
381+Gateway=2001:beef:beef::1
382+'''})
383+
384+ def test_route_v6_multiple(self):
385+ self.generate('''network:
386+ version: 2
387+ ethernets:
388+ enblue:
389+ addresses: ["192.168.1.3/24"]
390+ routes:
391+ - to: 2001:dead:beef::2/64
392+ via: 2001:beef:beef::1
393+ - to: 2001:f00f:f00f::fe/64
394+ via: 2001:beef:feed::1
395+ metric: 1024''')
396+
397+ self.assert_networkd({'enblue.network': '''[Match]
398+Name=enblue
399+
400+[Network]
401+Address=192.168.1.3/24
402+
403+[Route]
404+Destination=2001:dead:beef::2/64
405+Gateway=2001:beef:beef::1
406+
407+[Route]
408+Destination=2001:f00f:f00f::fe/64
409+Gateway=2001:beef:feed::1
410+Metric=1024
411+'''})
412+
413 def test_wifi(self):
414 self.generate('''network:
415 version: 2
416@@ -552,6 +664,38 @@ network={
417 self.assertTrue(os.path.islink(os.path.join(
418 self.workdir.name, 'run/systemd/system/multi-user.target.wants/netplan-wpa@wl0.service')))
419
420+ def test_wifi_route(self):
421+ self.generate('''network:
422+ version: 2
423+ wifis:
424+ wl0:
425+ access-points:
426+ workplace:
427+ password: "c0mpany"
428+ dhcp4: yes
429+ routes:
430+ - to: 10.10.10.0/24
431+ via: 8.8.8.8''')
432+
433+ self.assert_networkd({'wl0.network': '''[Match]
434+Name=wl0
435+
436+[Network]
437+DHCP=ipv4
438+
439+[Route]
440+Destination=10.10.10.0/24
441+Gateway=8.8.8.8
442+
443+[DHCP]
444+RouteMetric=600
445+'''})
446+
447+ self.assert_nm(None, '''[keyfile]
448+# devices managed by networkd
449+unmanaged-devices+=interface-name:wl0,''')
450+ self.assert_udev(None)
451+
452 def test_wifi_match(self):
453 err = self.generate('''network:
454 version: 2
455@@ -1180,6 +1324,172 @@ method=manual
456 address1=2001:FFfe::1/64
457 '''})
458
459+ def test_route_v4_single(self):
460+ self.generate('''network:
461+ version: 2
462+ renderer: NetworkManager
463+ ethernets:
464+ engreen:
465+ addresses: ["192.168.14.2/24"]
466+ routes:
467+ - to: 10.10.10.0/24
468+ via: 192.168.14.20
469+ metric: 100
470+ ''')
471+
472+ self.assert_nm({'engreen': '''[connection]
473+id=netplan-engreen
474+type=ethernet
475+interface-name=engreen
476+
477+[ethernet]
478+wake-on-lan=0
479+
480+[ipv4]
481+method=manual
482+address1=192.168.14.2/24
483+route1=10.10.10.0/24,192.168.14.20,100
484+'''})
485+
486+ def test_route_v4_multiple(self):
487+ self.generate('''network:
488+ version: 2
489+ renderer: NetworkManager
490+ ethernets:
491+ engreen:
492+ addresses: ["192.168.14.2/24"]
493+ routes:
494+ - to: 8.8.0.0/16
495+ via: 192.168.1.1
496+ metric: 5000
497+ - to: 10.10.10.8
498+ via: 192.168.1.2
499+ - to: 11.11.11.0/24
500+ via: 192.168.1.3
501+ metric: 9999
502+ ''')
503+
504+ self.assert_nm({'engreen': '''[connection]
505+id=netplan-engreen
506+type=ethernet
507+interface-name=engreen
508+
509+[ethernet]
510+wake-on-lan=0
511+
512+[ipv4]
513+method=manual
514+address1=192.168.14.2/24
515+route1=8.8.0.0/16,192.168.1.1,5000
516+route2=10.10.10.8,192.168.1.2
517+route3=11.11.11.0/24,192.168.1.3,9999
518+'''})
519+
520+ def test_route_v6_single(self):
521+ self.generate('''network:
522+ version: 2
523+ renderer: NetworkManager
524+ ethernets:
525+ enblue:
526+ addresses: ["2001:f00f:f00f::2/64"]
527+ routes:
528+ - to: 2001:dead:beef::2/64
529+ via: 2001:beef:beef::1''')
530+
531+ self.assert_nm({'enblue': '''[connection]
532+id=netplan-enblue
533+type=ethernet
534+interface-name=enblue
535+
536+[ethernet]
537+wake-on-lan=0
538+
539+[ipv4]
540+method=link-local
541+
542+[ipv6]
543+method=manual
544+address1=2001:f00f:f00f::2/64
545+route1=2001:dead:beef::2/64,2001:beef:beef::1
546+'''})
547+
548+ def test_route_v6_multiple(self):
549+ self.generate('''network:
550+ version: 2
551+ renderer: NetworkManager
552+ ethernets:
553+ enblue:
554+ addresses: ["2001:f00f:f00f::2/64"]
555+ routes:
556+ - to: 2001:dead:beef::2/64
557+ via: 2001:beef:beef::1
558+ - to: 2001:dead:feed::2/64
559+ via: 2001:beef:beef::2
560+ metric: 1000''')
561+
562+ self.assert_nm({'enblue': '''[connection]
563+id=netplan-enblue
564+type=ethernet
565+interface-name=enblue
566+
567+[ethernet]
568+wake-on-lan=0
569+
570+[ipv4]
571+method=link-local
572+
573+[ipv6]
574+method=manual
575+address1=2001:f00f:f00f::2/64
576+route1=2001:dead:beef::2/64,2001:beef:beef::1
577+route2=2001:dead:feed::2/64,2001:beef:beef::2,1000
578+'''})
579+
580+ def test_routes_mixed(self):
581+ self.generate('''network:
582+ version: 2
583+ renderer: NetworkManager
584+ ethernets:
585+ engreen:
586+ addresses: ["192.168.14.2/24", "2001:f00f::2/128"]
587+ routes:
588+ - to: 2001:dead:beef::2/64
589+ via: 2001:beef:beef::1
590+ metric: 997
591+ - to: 8.8.0.0/16
592+ via: 192.168.1.1
593+ metric: 5000
594+ - to: 10.10.10.8
595+ via: 192.168.1.2
596+ - to: 11.11.11.0/24
597+ via: 192.168.1.3
598+ metric: 9999
599+ - to: 2001:f00f:f00f::fe/64
600+ via: 2001:beef:feed::1
601+ ''')
602+
603+ self.assert_nm({'engreen': '''[connection]
604+id=netplan-engreen
605+type=ethernet
606+interface-name=engreen
607+
608+[ethernet]
609+wake-on-lan=0
610+
611+[ipv4]
612+method=manual
613+address1=192.168.14.2/24
614+route1=8.8.0.0/16,192.168.1.1,5000
615+route2=10.10.10.8,192.168.1.2
616+route3=11.11.11.0/24,192.168.1.3,9999
617+
618+[ipv6]
619+method=manual
620+address1=2001:f00f::2/128
621+route1=2001:dead:beef::2/64,2001:beef:beef::1,997
622+route2=2001:f00f:f00f::fe/64,2001:beef:feed::1
623+'''})
624+
625 def test_wifi_default(self):
626 self.generate('''network:
627 version: 2
628@@ -2077,6 +2387,95 @@ class TestConfigErrors(TestBase):
629 ena: {id: 1, link: en1}''', expect_fail=True)
630 self.assertIn('interface en1 is not defined\n', err)
631
632+ def test_device_bad_route_to(self):
633+ self.generate('''network:
634+ version: 2
635+ ethernets:
636+ engreen:
637+ routes:
638+ - to: badlocation
639+ via: 192.168.14.20
640+ metric: 100
641+ addresses:
642+ - 192.168.14.2/24
643+ - 2001:FFfe::1/64''', expect_fail=True)
644+
645+ def test_device_bad_route_via(self):
646+ self.generate('''network:
647+ version: 2
648+ ethernets:
649+ engreen:
650+ routes:
651+ - to: 10.10.0.0/16
652+ via: badgateway
653+ metric: 100
654+ addresses:
655+ - 192.168.14.2/24
656+ - 2001:FFfe::1/64''', expect_fail=True)
657+
658+ def test_device_bad_route_metric(self):
659+ self.generate('''network:
660+ version: 2
661+ ethernets:
662+ engreen:
663+ routes:
664+ - to: 10.10.0.0/16
665+ via: 10.1.1.1
666+ metric: -1
667+ addresses:
668+ - 192.168.14.2/24
669+ - 2001:FFfe::1/64''', expect_fail=True)
670+
671+ def test_device_route_family_mismatch_ipv6_to(self):
672+ self.generate('''network:
673+ version: 2
674+ ethernets:
675+ engreen:
676+ routes:
677+ - to: 2001:dead:beef::0/16
678+ via: 10.1.1.1
679+ metric: 1
680+ addresses:
681+ - 192.168.14.2/24
682+ - 2001:FFfe::1/64''', expect_fail=True)
683+
684+ def test_device_route_family_mismatch_ipv4_to(self):
685+ self.generate('''network:
686+ version: 2
687+ ethernets:
688+ engreen:
689+ routes:
690+ - via: 2001:dead:beef::2
691+ to: 10.10.10.0/24
692+ metric: 1
693+ addresses:
694+ - 192.168.14.2/24
695+ - 2001:FFfe::1/64''', expect_fail=True)
696+
697+ def test_device_route_missing_to(self):
698+ self.generate('''network:
699+ version: 2
700+ ethernets:
701+ engreen:
702+ routes:
703+ - via: 2001:dead:beef::2
704+ metric: 1
705+ addresses:
706+ - 192.168.14.2/24
707+ - 2001:FFfe::1/64''', expect_fail=True)
708+
709+ def test_device_route_missing_via(self):
710+ self.generate('''network:
711+ version: 2
712+ ethernets:
713+ engreen:
714+ routes:
715+ - to: 2001:dead:beef::2
716+ metric: 1
717+ addresses:
718+ - 192.168.14.2/24
719+ - 2001:FFfe::1/64''', expect_fail=True)
720+
721
722 class TestMerging(TestBase):
723 '''multiple *.yaml merging'''
724diff --git a/tests/integration.py b/tests/integration.py
725index 6897395..7242f32 100755
726--- a/tests/integration.py
727+++ b/tests/integration.py
728@@ -355,6 +355,7 @@ class NetworkTestBase(unittest.TestCase):
729
730
731 class _CommonTests:
732+
733 def test_eth_and_bridge(self):
734 self.setup_eth(None)
735 self.start_dnsmasq(None, self.dev_e2_ap)
736@@ -413,6 +414,78 @@ class _CommonTests:
737 with open('/sys/class/net/mybond/bonding/slaves') as f:
738 self.assertEqual(f.read().strip(), self.dev_e_client)
739
740+ @unittest.skip("fails due to networkd bug setting routes with dhcp")
741+ def test_routes_v4_with_dhcp(self):
742+ self.setup_eth(None)
743+ with open(self.config, 'w') as f:
744+ f.write('''network:
745+ renderer: %(r)s
746+ ethernets:
747+ %(ec)s:
748+ dhcp4: yes
749+ routes:
750+ - to: 10.10.10.0/24
751+ via: 192.168.5.254
752+ metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client})
753+ self.generate_and_settle()
754+ self.assert_iface_up(self.dev_e_client,
755+ ['inet 192.168.5.[0-9]+/24']) # from DHCP
756+ self.assertIn(b'default via 192.168.5.1', # from DHCP
757+ subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client]))
758+ self.assertIn(b'10.10.10.0/24 via 192.168.5.254', # from static route
759+ subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client]))
760+ self.assertIn(b'metric 99', # check metric from static route
761+ subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24']))
762+
763+ def test_routes_v4(self):
764+ self.setup_eth(None)
765+ with open(self.config, 'w') as f:
766+ f.write('''network:
767+ renderer: %(r)s
768+ ethernets:
769+ %(ec)s:
770+ addresses:
771+ - 192.168.5.99/24
772+ gateway4: 192.168.5.1
773+ routes:
774+ - to: 10.10.10.0/24
775+ via: 192.168.5.254
776+ metric: 99''' % {'r': self.backend, 'ec': self.dev_e_client})
777+ self.generate_and_settle()
778+ self.assert_iface_up(self.dev_e_client,
779+ ['inet 192.168.5.[0-9]+/24']) # from DHCP
780+ self.assertIn(b'default via 192.168.5.1', # from DHCP
781+ subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client]))
782+ self.assertIn(b'10.10.10.0/24 via 192.168.5.254', # from DHCP
783+ subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client]))
784+ self.assertIn(b'metric 99', # check metric from static route
785+ subprocess.check_output(['ip', 'route', 'show', '10.10.10.0/24']))
786+
787+ def test_routes_v6(self):
788+ self.setup_eth(None)
789+ with open(self.config, 'w') as f:
790+ f.write('''network:
791+ renderer: %(r)s
792+ ethernets:
793+ %(ec)s:
794+ addresses: ["9876:BBBB::11/70"]
795+ gateway6: "9876:BBBB::1"
796+ routes:
797+ - to: 2001:f00f:f00f::1/64
798+ via: 9876:BBBB::5
799+ metric: 799''' % {'r': self.backend, 'ec': self.dev_e_client})
800+ self.generate_and_settle()
801+ self.assert_iface_up(self.dev_e_client,
802+ ['inet6 9876:bbbb::11/70'])
803+ self.assertNotIn(b'default',
804+ subprocess.check_output(['ip', 'route', 'show', 'dev', self.dev_e_client]))
805+ self.assertIn(b'default via 9876:bbbb::1',
806+ subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client]))
807+ self.assertIn(b'2001:f00f:f00f::/64 via 9876:bbbb::5',
808+ subprocess.check_output(['ip', '-6', 'route', 'show', 'dev', self.dev_e_client]))
809+ self.assertIn(b'metric 799',
810+ subprocess.check_output(['ip', '-6', 'route', 'show', '2001:f00f:f00f::/64']))
811+
812 def test_manual_addresses(self):
813 self.setup_eth(None)
814 with open(self.config, 'w') as f:

Subscribers

People subscribed via source and target branches

to all changes: