Merge lp:~mpontillo/maas/netplan--part2--tests into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 5785
Proposed branch: lp:~mpontillo/maas/netplan--part2--tests
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 686 lines (+430/-105)
2 files modified
src/maasserver/preseed_network.py (+261/-104)
src/maasserver/tests/test_preseed_network.py (+169/-1)
To merge this branch: bzr merge lp:~mpontillo/maas/netplan--part2--tests
Reviewer Review Type Date Requested Status
Mike Pontillo (community) Approve
Blake Rouse Pending
Review via email: mp+319039@code.launchpad.net

This proposal supersedes a proposal from 2017-02-19.

Commit message

Support netplan (v2 YAML) rendering in the network preseed generator.

Implement tests for Netplan YAML.Support netplan (v2 YAML) rendering in the network preseed generator.

Drive-by fix to allow preseed YAML to be rendered for nodes with NULL IP addresses (such as AUTO addresses that have not been assigned yet) to be rendered (without those addresses). It would be nice if a special syntax existed for "TBD" addresses, so we could show these for debugging purposes.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote : Posted in a previous version of this proposal

Looks good.

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Approved by Blake (landing this as one branch).

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (32.7 KiB)

The attempt to merge lp:~mpontillo/maas/netplan--part2--tests into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease
Get:2 http://security.ubuntu.com/ubuntu xenial-security InRelease [102 kB]
Get:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease [102 kB]
Get:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease [102 kB]
Fetched 306 kB in 0s (602 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind avahi-utils bash bind9 bind9utils build-essential bzr bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common isc-dhcp-server libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libnss-wrapper libpq-dev make nodejs-legacy npm postgresql psmisc pxelinux python3-all python3-apt python3-attr python3-bson python3-convoy python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-dnspython python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-netaddr python3-netifaces python3-novaclient python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-simplejson python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
authbind is already the newest version (2.1.1+nmu1).
avahi-utils is already the newest version (0.6.32~rc+dfsg-1ubuntu2).
build-essential is already the newest version (12.1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu3).
distro-info is already the newest version (0.14build1).
git is already the newest version (1:2.7.4-0ubuntu1).
libjs-angularjs is already the newest version (1.2.28-1ubuntu2).
libjs-jquery is already the newest version (1.11.3+dfsg-4).
libjs-yui3-full is already the newest version (3.5.1-1ubuntu3).
libjs-yui3-min is already the newest version (3.5.1-1ubuntu3).
make is already the newest version (4.1-6).
postgresql is already the newest version (9.5+173).
psmisc is already the newest version (22.21-2.1build1).
pxelinux is already the newest version (3:6.03+dfsg-11ubuntu1).
python-formencode is already the newest version (1.3.0-0ubuntu5).
python-lxml is already the newest version (3.5.0-1build1).
python-netaddr is already the newest version (0.7.18-1).
python-net...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/preseed_network.py'
2--- src/maasserver/preseed_network.py 2017-02-17 14:23:04 +0000
3+++ src/maasserver/preseed_network.py 2017-03-06 11:48:26 +0000
4@@ -17,7 +17,7 @@
5 )
6 from maasserver.models import Interface
7 from maasserver.models.staticroute import StaticRoute
8-from netaddr import IPAddress
9+from netaddr import IPNetwork
10 import yaml
11
12
13@@ -45,21 +45,29 @@
14 return value
15
16
17-def _generate_route_operation(route):
18+def _generate_route_operation(route, version=1):
19 """Generate route operation place in `network_config`."""
20- route_operation = {
21- "id": route.id,
22- "type": "route",
23- "destination": route.destination.cidr,
24- "gateway": route.gateway_ip,
25- "metric": route.metric,
26- }
27- return route_operation
28+ if version == 1:
29+ route_operation = {
30+ "id": route.id,
31+ "type": "route",
32+ "destination": route.destination.cidr,
33+ "gateway": route.gateway_ip,
34+ "metric": route.metric,
35+ }
36+ return route_operation
37+ elif version == 2:
38+ route_operation = {
39+ "to": route.destination.cidr,
40+ "via": route.gateway_ip,
41+ "metric": route.metric,
42+ }
43+ return route_operation
44
45
46 class InterfaceConfiguration:
47
48- def __init__(self, iface, node_config):
49+ def __init__(self, iface, node_config, version=1):
50 """
51
52 :param iface: The interface whose configuration to generate.
53@@ -73,32 +81,45 @@
54 self.gateways = node_config.gateways
55 self.matching_routes = set()
56 self.addr_family_present = defaultdict(bool)
57+ self.version = version
58 self.config = None
59+ self.name = self.iface.get_name()
60
61 if self.type == INTERFACE_TYPE.PHYSICAL:
62- self.config = self._generate_physical_operation()
63+ self.config = self._generate_physical_operation(version=version)
64 elif self.type == INTERFACE_TYPE.VLAN:
65- self.config = self._generate_vlan_operation()
66+ self.config = self._generate_vlan_operation(version=version)
67 elif self.type == INTERFACE_TYPE.BOND:
68- self.config = self._generate_bond_operation()
69+ self.config = self._generate_bond_operation(version=version)
70 elif self.type == INTERFACE_TYPE.BRIDGE:
71- self.config = self._generate_bridge_operation()
72+ self.config = self._generate_bridge_operation(version=version)
73 else:
74 raise ValueError("Unknown interface type: %s" % self.type)
75
76- def _generate_physical_operation(self):
77+ def _generate_physical_operation(self, version=1):
78 """Generate physical interface operation for `interface` and place in
79 `network_config`."""
80- addrs = self._generate_addresses()
81+ addrs = self._generate_addresses(version=version)
82 physical_operation = self._get_initial_params()
83- physical_operation.update({
84- "id": self.iface.get_name(),
85- "type": "physical",
86- "name": self.iface .get_name(),
87- "mac_address": str(self.iface .mac_address),
88- })
89- if addrs:
90- physical_operation["subnets"] = addrs
91+ if version == 1:
92+ physical_operation.update({
93+ "id": self.name,
94+ "type": "physical",
95+ "name": self.name,
96+ "mac_address": str(self.iface.mac_address),
97+ })
98+ if addrs:
99+ physical_operation["subnets"] = addrs
100+ elif version == 2:
101+ physical_operation.update({
102+ "match": {
103+ "macaddress": str(self.iface.mac_address),
104+ },
105+ # Unclear what we want, so just let it be the default.
106+ # "wakeonlan": True,
107+ "set-name": self.name,
108+ })
109+ physical_operation.update(addrs)
110 return physical_operation
111
112 def _get_dhcp_type(self):
113@@ -141,7 +162,7 @@
114 return subnet.gateway_ip
115 return None
116
117- def _set_default_gateway(self, subnet, subnet_operation):
118+ def _set_default_gateway(self, subnet, config, version=1):
119 """Set the default gateway on the `subnet_operation` if it should
120 be set."""
121 family = subnet.get_ipnetwork().version
122@@ -152,11 +173,16 @@
123 return
124 gateway = self._get_default_gateway(subnet)
125 if gateway is not None:
126+ if version == 1:
127+ config["gateway"] = str(gateway)
128 if family == IPADDRESS_FAMILY.IPv4:
129 node_config.gateway_ipv4_set = True
130+ if version == 2:
131+ config["gateway4"] = str(gateway)
132 elif family == IPADDRESS_FAMILY.IPv6:
133 node_config.gateway_ipv6_set = True
134- subnet_operation["gateway"] = str(gateway)
135+ if version == 2:
136+ config["gateway6"] = str(gateway)
137
138 def _get_matching_routes(self, source):
139 """Return all route objects matching `source`."""
140@@ -166,9 +192,12 @@
141 if route.source == source
142 }
143
144- def _generate_addresses(self):
145+ def _generate_addresses(self, version=1):
146 """Generate the various addresses needed for this interface."""
147- addrs = []
148+ v1_config = []
149+ v2_cidrs = []
150+ v2_config = {}
151+ v2_nameservers = {}
152 addresses = list(
153 self.iface.ip_addresses.exclude(
154 alloc_type__in=[
155@@ -177,83 +206,162 @@
156 ]).order_by('id'))
157 dhcp_type = self._get_dhcp_type()
158 if _is_link_up(addresses) and not dhcp_type:
159- addrs.append({"type": "manual"})
160+ if version == 1:
161+ v1_config.append({"type": "manual"})
162 else:
163 for address in addresses:
164 subnet = address.subnet
165 if subnet is not None:
166 subnet_len = subnet.cidr.split('/')[1]
167- subnet_operation = {
168+ cidr = "%s/%s" % (str(address.ip), subnet_len)
169+ v1_subnet_operation = {
170 "type": "static",
171- "address": "%s/%s" % (str(address.ip), subnet_len)
172+ "address": cidr
173 }
174+ if address.ip is not None:
175+ # If the address is None, that means we're generating a
176+ # preseed for a Node that is not (or is no longer) in
177+ # the READY state; so it might have auto-assigned IP
178+ # addresses which have not yet been determined. It
179+ # would be nice if there was a way to express this, if
180+ # only for debugging purposes. For now, just ignore
181+ # such addresses.
182+ v1_subnet_operation['address'] = cidr
183+ v2_cidrs.append(cidr)
184+ if "addresses" not in v2_config:
185+ v2_config["addresses"] = v2_cidrs
186+ v1_config.append(v1_subnet_operation)
187 self.addr_family_present[
188- IPAddress(address.ip).version] = True
189+ IPNetwork(subnet.cidr).version] = True
190+ # The default gateway is set on the subnet operation for
191+ # the v1 YAML, but it's per-interface for the v2 YAML.
192 self._set_default_gateway(
193- subnet, subnet_operation)
194+ subnet,
195+ v1_subnet_operation if version == 1 else v2_config)
196 if subnet.dns_servers is not None:
197- subnet_operation["dns_nameservers"] = (
198+ v1_subnet_operation["dns_nameservers"] = (
199 subnet.dns_servers)
200- addrs.append(subnet_operation)
201+ if "nameservers" not in v2_config:
202+ v2_config["nameservers"] = v2_nameservers
203+ # XXX should also support search paths.
204+ if "addresses" not in v2_nameservers:
205+ v2_nameservers["addresses"] = []
206+ v2_nameservers["addresses"].extend(
207+ [server for server in subnet.dns_servers])
208 self.matching_routes.update(
209 self._get_matching_routes(subnet))
210 if dhcp_type:
211- addrs.append(
212+ v1_config.append(
213 {"type": dhcp_type}
214 )
215- return addrs
216+ if dhcp_type == "dhcp":
217+ v2_config.update({
218+ "dhcp4": True,
219+ "dhcp6": True,
220+ })
221+ elif dhcp_type == "dhcp4":
222+ v2_config.update({
223+ "dhcp4": True,
224+ })
225+ elif dhcp_type == "dhcp6":
226+ v2_config.update({
227+ "dhcp6": True,
228+ })
229+ if version == 1:
230+ return v1_config
231+ elif version == 2:
232+ return v2_config
233
234- def _generate_vlan_operation(self):
235+ def _generate_vlan_operation(self, version=1):
236 """Generate vlan operation for `iface` and place in
237 `network_config`."""
238 vlan = self.iface.vlan
239- name = self.iface.get_name()
240- addrs = self._generate_addresses()
241+ name = self.name
242+ addrs = self._generate_addresses(version=version)
243 vlan_operation = self._get_initial_params()
244- vlan_operation.update({
245- "id": name,
246- "type": "vlan",
247- "name": name,
248- "vlan_link": self.iface.parents.first().get_name(),
249- "vlan_id": vlan.vid,
250- })
251- if addrs:
252- vlan_operation["subnets"] = addrs
253+ if version == 1:
254+ vlan_operation.update({
255+ "id": name,
256+ "type": "vlan",
257+ "name": name,
258+ "vlan_link": self.iface.parents.first().get_name(),
259+ "vlan_id": vlan.vid,
260+ })
261+ if addrs:
262+ vlan_operation["subnets"] = addrs
263+ elif version == 2:
264+ vlan_operation.update({
265+ "id": vlan.vid,
266+ "link": self.iface.parents.first().get_name(),
267+ })
268+ vlan_operation.update(addrs)
269 return vlan_operation
270
271- def _generate_bond_operation(self):
272+ def _generate_bond_operation(self, version=1):
273 """Generate bond operation for `iface` and place in
274 `network_config`."""
275- addrs = self._generate_addresses()
276+ addrs = self._generate_addresses(version=version)
277 bond_operation = self._get_initial_params()
278- bond_operation.update({
279- "id": self.iface.get_name(),
280- "type": "bond",
281- "name": self.iface.get_name(),
282- "mac_address": str(self.iface.mac_address),
283- "bond_interfaces": [parent.get_name() for parent in
284- self.iface.parents.order_by('name')],
285- "params": self._get_bond_params(),
286- })
287- if addrs:
288- bond_operation["subnets"] = addrs
289+ if version == 1:
290+ bond_operation.update({
291+ "id": self.name,
292+ "type": "bond",
293+ "name": self.name,
294+ "mac_address": str(self.iface.mac_address),
295+ "bond_interfaces": [parent.get_name() for parent in
296+ self.iface.parents.order_by('name')],
297+ "params": self._get_bond_params(),
298+ })
299+ if addrs:
300+ bond_operation["subnets"] = addrs
301+ else:
302+ bond_operation.update({
303+ # XXX mpontillo 2017-02-17: netplan does not yet support
304+ # specifying the MAC that should be used for a bond.
305+ # See launchpad bug #1664698.
306+ # "macaddress": str(self.iface.mac_address),
307+ "interfaces": [
308+ parent.get_name()
309+ for parent in self.iface.parents.order_by('name')
310+ ],
311+ # XXX mpontillo 2017-02-17: netplan does not yet support
312+ # specifying bond parameters. See launchpad bug #1664702.
313+ # "params": self._get_bond_params(),
314+ })
315+ bond_operation.update(addrs)
316 return bond_operation
317
318- def _generate_bridge_operation(self):
319+ def _generate_bridge_operation(self, version=1):
320 """Generate bridge operation for this interface."""
321- addrs = self._generate_addresses()
322+ addrs = self._generate_addresses(version=version)
323 bridge_operation = self._get_initial_params()
324- bridge_operation.update({
325- "id": self.iface.get_name(),
326- "type": "bridge",
327- "name": self.iface.get_name(),
328- "mac_address": str(self.iface.mac_address),
329- "bridge_interfaces": [parent.get_name() for parent in
330- self.iface.parents.order_by('name')],
331- "params": self._get_bridge_params(),
332- })
333- if addrs:
334- bridge_operation["subnets"] = addrs
335+ if version == 1:
336+ bridge_operation.update({
337+ "id": self.name,
338+ "type": "bridge",
339+ "name": self.name,
340+ "mac_address": str(self.iface.mac_address),
341+ "bridge_interfaces": [parent.get_name() for parent in
342+ self.iface.parents.order_by('name')],
343+ "params": self._get_bridge_params(),
344+ })
345+ if addrs:
346+ bridge_operation["subnets"] = addrs
347+ elif version == 2:
348+ bridge_operation.update({
349+ # XXX mpontillo 2017-02-17: netplan does not yet support
350+ # specifying the MAC that should be used for a bond.
351+ # See launchpad bug #1664698.
352+ # "macaddress": str(self.iface.mac_address),
353+ "interfaces": [
354+ parent.get_name()
355+ for parent in self.iface.parents.order_by('name')
356+ ],
357+ # XXX mpontillo 2017-02-17: netplan does not yet support
358+ # specifying bridge parameters. See launchpad bug #1664702.
359+ # "params": self._get_bridge_params(),
360+ })
361+ bridge_operation.update(addrs)
362 return bridge_operation
363
364 def _get_initial_params(self):
365@@ -299,13 +407,20 @@
366 class NodeNetworkConfiguration:
367 """Generator for the YAML network configuration for curtin."""
368
369- def __init__(self, node):
370+ def __init__(self, node, version=1):
371 """Create the YAML network configuration for the specified node, and
372 store it in the `config` ivar.
373 """
374 self.node = node
375 self.matching_routes = set()
376- self.network_config = []
377+ self.v1_config = []
378+ self.v2_config = {
379+ "version": 2
380+ }
381+ self.v2_ethernets = {}
382+ self.v2_vlans = {}
383+ self.v2_bonds = {}
384+ self.v2_bridges = {}
385 self.gateway_ipv4_set = False
386 self.gateway_ipv6_set = False
387 # The default value is False: expected keys are 4 and 6.
388@@ -318,13 +433,21 @@
389 for iface in interfaces:
390 if not iface.is_enabled():
391 continue
392- generator = InterfaceConfiguration(iface, self)
393+ generator = InterfaceConfiguration(iface, self, version=version)
394 self.matching_routes.update(generator.matching_routes)
395 self.addr_family_present.update(generator.addr_family_present)
396- self.network_config.append(generator.config)
397-
398- # Generate each YAML operation in the network_config.
399- self._generate_route_operations()
400+ if version == 1:
401+ self.v1_config.append(generator.config)
402+ elif version == 2:
403+ v2_config = {generator.name: generator.config}
404+ if generator.type == INTERFACE_TYPE.PHYSICAL:
405+ self.v2_ethernets.update(v2_config)
406+ elif generator.type == INTERFACE_TYPE.VLAN:
407+ self.v2_vlans.update(v2_config)
408+ elif generator.type == INTERFACE_TYPE.BOND:
409+ self.v2_bonds.update(v2_config)
410+ elif generator.type == INTERFACE_TYPE.BRIDGE:
411+ self.v2_bridges.update(v2_config)
412
413 # If we have no IPv6 addresses present, make sure we claim IPv4, so
414 # that we at least get some address.
415@@ -336,31 +459,65 @@
416 name
417 for name in sorted(get_dns_search_paths())
418 if name != self.node.domain.name]
419- self.network_config.append({
420+ self._generate_route_operations(version=version)
421+ self.v1_config.append({
422 "type": "nameserver",
423 "address": default_dns_servers,
424 "search": search_list,
425 })
426-
427- network_config = {
428- "network_commands": {
429- "builtin": ["curtin", "net-meta", "custom"],
430- },
431- "network": {
432- "version": 1,
433- "config": self.network_config,
434- },
435- }
436- # Render the resulting YAML.
437- self.config = yaml.safe_dump(network_config, default_flow_style=False)
438-
439- def _generate_route_operations(self):
440+ if version == 1:
441+ network_config = {
442+ "network": {
443+ "version": 1,
444+ "config": self.v1_config,
445+ },
446+ }
447+ else:
448+ network_config = {
449+ "network": self.v2_config,
450+ }
451+ v2_config = network_config['network']
452+ if len(self.v2_ethernets) > 0:
453+ v2_config.update({"ethernets": self.v2_ethernets})
454+ if len(self.v2_vlans) > 0:
455+ v2_config.update({"vlans": self.v2_vlans})
456+ if len(self.v2_bonds) > 0:
457+ v2_config.update({"bonds": self.v2_bonds})
458+ if len(self.v2_bridges) > 0:
459+ v2_config.update({"bridges": self.v2_bridges})
460+ # XXX mpontillo 2017-02-17: netplan has no concept of "default"
461+ # DNS servers. Need to define how to convey this.
462+ # See launchpad bug #1664806.
463+ # if len(default_dns_servers) > 0 or len(search_list) > 0:
464+ # nameservers = {}
465+ # if len(search_list) > 0:
466+ # nameservers.update({"search": search_list})
467+ # if len(default_dns_servers) > 0:
468+ # nameservers.update({"addresses": default_dns_servers})
469+ # v2_config.update({"nameservers": nameservers})
470+ self.config = network_config
471+
472+ def _generate_route_operations(self, version=1):
473 """Generate all route operations."""
474+ routes = []
475 for route in sorted(self.matching_routes, key=attrgetter("id")):
476- self.network_config.append(_generate_route_operation(route))
477-
478-
479-def compose_curtin_network_config(node):
480+ routes.append(_generate_route_operation(route, version=version))
481+ if version == 1:
482+ self.v1_config.extend(routes)
483+ elif version == 2 and len(routes) > 0:
484+ self.v2_config["routes"] = routes
485+
486+
487+def compose_curtin_network_config(node, version=1):
488 """Compose the network configuration for curtin."""
489- generator = NodeNetworkConfiguration(node)
490- return [generator.config]
491+ generator = NodeNetworkConfiguration(node, version=version)
492+ curtin_config = {
493+ "network_commands": {
494+ "builtin": ["curtin", "net-meta", "custom"],
495+ }
496+ }
497+ curtin_config.update(generator.config)
498+ # Render the resulting YAML.
499+ curtin_config_yaml = yaml.safe_dump(
500+ curtin_config, default_flow_style=False)
501+ return [curtin_config_yaml]
502
503=== modified file 'src/maasserver/tests/test_preseed_network.py'
504--- src/maasserver/tests/test_preseed_network.py 2017-02-22 14:55:09 +0000
505+++ src/maasserver/tests/test_preseed_network.py 2017-03-06 11:48:26 +0000
506@@ -16,7 +16,10 @@
507 IPADDRESS_TYPE,
508 )
509 from maasserver.models.staticroute import StaticRoute
510-from maasserver.preseed_network import compose_curtin_network_config
511+from maasserver.preseed_network import (
512+ compose_curtin_network_config,
513+ NodeNetworkConfiguration,
514+)
515 from maasserver.testing.factory import factory
516 from maasserver.testing.testcase import MAASServerTestCase
517 from netaddr import IPNetwork
518@@ -461,3 +464,168 @@
519 net_config += self.collectDNSConfig(node)
520 config = compose_curtin_network_config(node)
521 self.assertNetworkConfig(net_config, config)
522+
523+
524+class TestNetplan(MAASServerTestCase):
525+
526+ def _render_netplan_dict(self, node):
527+ return NodeNetworkConfiguration(node, version=2).config
528+
529+ def test__single_ethernet_interface(self):
530+ node = factory.make_Node()
531+ factory.make_Interface(
532+ node=node, name='eth0', mac_address="00:01:02:03:04:05")
533+ netplan = self._render_netplan_dict(node)
534+ expected_netplan = {
535+ 'network': {
536+ 'version': 2,
537+ 'ethernets': {
538+ 'eth0': {
539+ 'match': {'macaddress': '00:01:02:03:04:05'},
540+ 'mtu': 1500,
541+ 'set-name': 'eth0'
542+ },
543+ },
544+ }
545+ }
546+ self.expectThat(netplan, Equals(expected_netplan))
547+
548+ def test__multiple_ethernet_interfaces(self):
549+ node = factory.make_Node()
550+ factory.make_Interface(
551+ node=node, name='eth0', mac_address="00:01:02:03:04:05")
552+ factory.make_Interface(
553+ node=node, name='eth1', mac_address="02:01:02:03:04:05")
554+ netplan = self._render_netplan_dict(node)
555+ expected_netplan = {
556+ 'network': {
557+ 'version': 2,
558+ 'ethernets': {
559+ 'eth0': {
560+ 'match': {'macaddress': '00:01:02:03:04:05'},
561+ 'mtu': 1500,
562+ 'set-name': 'eth0'
563+ },
564+ 'eth1': {
565+ 'match': {'macaddress': '02:01:02:03:04:05'},
566+ 'mtu': 1500,
567+ 'set-name': 'eth1'
568+ },
569+ },
570+ },
571+ }
572+ self.expectThat(netplan, Equals(expected_netplan))
573+
574+ def test__bond(self):
575+ node = factory.make_Node()
576+ eth0 = factory.make_Interface(
577+ node=node, name='eth0', mac_address="00:01:02:03:04:05")
578+ eth1 = factory.make_Interface(
579+ node=node, name='eth1', mac_address="02:01:02:03:04:05")
580+ factory.make_Interface(
581+ INTERFACE_TYPE.BOND,
582+ node=node, name='bond0', parents=[eth0, eth1])
583+ netplan = self._render_netplan_dict(node)
584+ expected_netplan = {
585+ 'network': {
586+ 'version': 2,
587+ 'ethernets': {
588+ 'eth0': {
589+ 'match': {'macaddress': '00:01:02:03:04:05'},
590+ 'mtu': 1500,
591+ 'set-name': 'eth0'
592+ },
593+ 'eth1': {
594+ 'match': {'macaddress': '02:01:02:03:04:05'},
595+ 'mtu': 1500,
596+ 'set-name': 'eth1'
597+ },
598+ },
599+ 'bonds': {
600+ 'bond0': {
601+ 'interfaces': ['eth0', 'eth1'],
602+ 'mtu': 1500
603+ },
604+ },
605+ }
606+ }
607+ self.expectThat(netplan, Equals(expected_netplan))
608+
609+ def test__bridge(self):
610+ node = factory.make_Node()
611+ eth0 = factory.make_Interface(
612+ node=node, name='eth0', mac_address="00:01:02:03:04:05")
613+ eth1 = factory.make_Interface(
614+ node=node, name='eth1', mac_address="02:01:02:03:04:05")
615+ factory.make_Interface(
616+ INTERFACE_TYPE.BRIDGE,
617+ node=node, name='br0', parents=[eth0, eth1])
618+ netplan = self._render_netplan_dict(node)
619+ expected_netplan = {
620+ 'network': {
621+ 'version': 2,
622+ 'ethernets': {
623+ 'eth0': {
624+ 'match': {'macaddress': '00:01:02:03:04:05'},
625+ 'mtu': 1500,
626+ 'set-name': 'eth0'
627+ },
628+ 'eth1': {
629+ 'match': {'macaddress': '02:01:02:03:04:05'},
630+ 'mtu': 1500,
631+ 'set-name': 'eth1'
632+ },
633+ },
634+ 'bridges': {
635+ 'br0': {
636+ 'interfaces': ['eth0', 'eth1'],
637+ 'mtu': 1500
638+ },
639+ },
640+ }
641+ }
642+ self.expectThat(netplan, Equals(expected_netplan))
643+
644+ def test__bridged_bond(self):
645+ node = factory.make_Node()
646+ eth0 = factory.make_Interface(
647+ node=node, name='eth0', mac_address="00:01:02:03:04:05")
648+ eth1 = factory.make_Interface(
649+ node=node, name='eth1', mac_address="02:01:02:03:04:05")
650+ bond0 = factory.make_Interface(
651+ INTERFACE_TYPE.BOND,
652+ node=node, name='bond0', parents=[eth0, eth1])
653+ factory.make_Interface(
654+ INTERFACE_TYPE.BRIDGE,
655+ node=node, name='br0', parents=[bond0])
656+ netplan = self._render_netplan_dict(node)
657+ expected_netplan = {
658+ 'network': {
659+ 'version': 2,
660+ 'ethernets': {
661+ 'eth0': {
662+ 'match': {'macaddress': '00:01:02:03:04:05'},
663+ 'mtu': 1500,
664+ 'set-name': 'eth0'
665+ },
666+ 'eth1': {
667+ 'match': {'macaddress': '02:01:02:03:04:05'},
668+ 'mtu': 1500,
669+ 'set-name': 'eth1'
670+ },
671+ },
672+ 'bonds': {
673+ 'bond0': {
674+ 'interfaces': ['eth0', 'eth1'],
675+ 'mtu': 1500
676+ },
677+ },
678+ 'bridges': {
679+ 'br0': {
680+ 'interfaces': ['bond0'],
681+ 'mtu': 1500
682+ },
683+ },
684+ }
685+ }
686+ self.expectThat(netplan, Equals(expected_netplan))