Merge netplan:cyphermox/routes into netplan:master
- Git
- lp:netplan
- cyphermox/routes
- Merge into 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) |
| Related bugs: |
| 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:
|
|||
Commit Message
Description of the Change
Add support for defining routes.
To post a comment you must log in.
| 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
| 1 | diff --git a/src/networkd.c b/src/networkd.c |
| 2 | index 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) |
| 21 | diff --git a/src/nm.c b/src/nm.c |
| 22 | index 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); |
| 78 | diff --git a/src/parse.c b/src/parse.c |
| 79 | index 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 | |
| 262 | diff --git a/src/parse.h b/src/parse.h |
| 263 | index 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; |
| 293 | diff --git a/tests/generate.py b/tests/generate.py |
| 294 | index 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''' |
| 724 | diff --git a/tests/integration.py b/tests/integration.py |
| 725 | index 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: |

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.