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
=== modified file 'src/maasserver/preseed_network.py'
--- src/maasserver/preseed_network.py 2017-02-17 14:23:04 +0000
+++ src/maasserver/preseed_network.py 2017-03-06 11:48:26 +0000
@@ -17,7 +17,7 @@
17)17)
18from maasserver.models import Interface18from maasserver.models import Interface
19from maasserver.models.staticroute import StaticRoute19from maasserver.models.staticroute import StaticRoute
20from netaddr import IPAddress20from netaddr import IPNetwork
21import yaml21import yaml
2222
2323
@@ -45,21 +45,29 @@
45 return value45 return value
4646
4747
48def _generate_route_operation(route):48def _generate_route_operation(route, version=1):
49 """Generate route operation place in `network_config`."""49 """Generate route operation place in `network_config`."""
50 route_operation = {50 if version == 1:
51 "id": route.id,51 route_operation = {
52 "type": "route",52 "id": route.id,
53 "destination": route.destination.cidr,53 "type": "route",
54 "gateway": route.gateway_ip,54 "destination": route.destination.cidr,
55 "metric": route.metric,55 "gateway": route.gateway_ip,
56 }56 "metric": route.metric,
57 return route_operation57 }
58 return route_operation
59 elif version == 2:
60 route_operation = {
61 "to": route.destination.cidr,
62 "via": route.gateway_ip,
63 "metric": route.metric,
64 }
65 return route_operation
5866
5967
60class InterfaceConfiguration:68class InterfaceConfiguration:
6169
62 def __init__(self, iface, node_config):70 def __init__(self, iface, node_config, version=1):
63 """71 """
6472
65 :param iface: The interface whose configuration to generate.73 :param iface: The interface whose configuration to generate.
@@ -73,32 +81,45 @@
73 self.gateways = node_config.gateways81 self.gateways = node_config.gateways
74 self.matching_routes = set()82 self.matching_routes = set()
75 self.addr_family_present = defaultdict(bool)83 self.addr_family_present = defaultdict(bool)
84 self.version = version
76 self.config = None85 self.config = None
86 self.name = self.iface.get_name()
7787
78 if self.type == INTERFACE_TYPE.PHYSICAL:88 if self.type == INTERFACE_TYPE.PHYSICAL:
79 self.config = self._generate_physical_operation()89 self.config = self._generate_physical_operation(version=version)
80 elif self.type == INTERFACE_TYPE.VLAN:90 elif self.type == INTERFACE_TYPE.VLAN:
81 self.config = self._generate_vlan_operation()91 self.config = self._generate_vlan_operation(version=version)
82 elif self.type == INTERFACE_TYPE.BOND:92 elif self.type == INTERFACE_TYPE.BOND:
83 self.config = self._generate_bond_operation()93 self.config = self._generate_bond_operation(version=version)
84 elif self.type == INTERFACE_TYPE.BRIDGE:94 elif self.type == INTERFACE_TYPE.BRIDGE:
85 self.config = self._generate_bridge_operation()95 self.config = self._generate_bridge_operation(version=version)
86 else:96 else:
87 raise ValueError("Unknown interface type: %s" % self.type)97 raise ValueError("Unknown interface type: %s" % self.type)
8898
89 def _generate_physical_operation(self):99 def _generate_physical_operation(self, version=1):
90 """Generate physical interface operation for `interface` and place in100 """Generate physical interface operation for `interface` and place in
91 `network_config`."""101 `network_config`."""
92 addrs = self._generate_addresses()102 addrs = self._generate_addresses(version=version)
93 physical_operation = self._get_initial_params()103 physical_operation = self._get_initial_params()
94 physical_operation.update({104 if version == 1:
95 "id": self.iface.get_name(),105 physical_operation.update({
96 "type": "physical",106 "id": self.name,
97 "name": self.iface .get_name(),107 "type": "physical",
98 "mac_address": str(self.iface .mac_address),108 "name": self.name,
99 })109 "mac_address": str(self.iface.mac_address),
100 if addrs:110 })
101 physical_operation["subnets"] = addrs111 if addrs:
112 physical_operation["subnets"] = addrs
113 elif version == 2:
114 physical_operation.update({
115 "match": {
116 "macaddress": str(self.iface.mac_address),
117 },
118 # Unclear what we want, so just let it be the default.
119 # "wakeonlan": True,
120 "set-name": self.name,
121 })
122 physical_operation.update(addrs)
102 return physical_operation123 return physical_operation
103124
104 def _get_dhcp_type(self):125 def _get_dhcp_type(self):
@@ -141,7 +162,7 @@
141 return subnet.gateway_ip162 return subnet.gateway_ip
142 return None163 return None
143164
144 def _set_default_gateway(self, subnet, subnet_operation):165 def _set_default_gateway(self, subnet, config, version=1):
145 """Set the default gateway on the `subnet_operation` if it should166 """Set the default gateway on the `subnet_operation` if it should
146 be set."""167 be set."""
147 family = subnet.get_ipnetwork().version168 family = subnet.get_ipnetwork().version
@@ -152,11 +173,16 @@
152 return173 return
153 gateway = self._get_default_gateway(subnet)174 gateway = self._get_default_gateway(subnet)
154 if gateway is not None:175 if gateway is not None:
176 if version == 1:
177 config["gateway"] = str(gateway)
155 if family == IPADDRESS_FAMILY.IPv4:178 if family == IPADDRESS_FAMILY.IPv4:
156 node_config.gateway_ipv4_set = True179 node_config.gateway_ipv4_set = True
180 if version == 2:
181 config["gateway4"] = str(gateway)
157 elif family == IPADDRESS_FAMILY.IPv6:182 elif family == IPADDRESS_FAMILY.IPv6:
158 node_config.gateway_ipv6_set = True183 node_config.gateway_ipv6_set = True
159 subnet_operation["gateway"] = str(gateway)184 if version == 2:
185 config["gateway6"] = str(gateway)
160186
161 def _get_matching_routes(self, source):187 def _get_matching_routes(self, source):
162 """Return all route objects matching `source`."""188 """Return all route objects matching `source`."""
@@ -166,9 +192,12 @@
166 if route.source == source192 if route.source == source
167 }193 }
168194
169 def _generate_addresses(self):195 def _generate_addresses(self, version=1):
170 """Generate the various addresses needed for this interface."""196 """Generate the various addresses needed for this interface."""
171 addrs = []197 v1_config = []
198 v2_cidrs = []
199 v2_config = {}
200 v2_nameservers = {}
172 addresses = list(201 addresses = list(
173 self.iface.ip_addresses.exclude(202 self.iface.ip_addresses.exclude(
174 alloc_type__in=[203 alloc_type__in=[
@@ -177,83 +206,162 @@
177 ]).order_by('id'))206 ]).order_by('id'))
178 dhcp_type = self._get_dhcp_type()207 dhcp_type = self._get_dhcp_type()
179 if _is_link_up(addresses) and not dhcp_type:208 if _is_link_up(addresses) and not dhcp_type:
180 addrs.append({"type": "manual"})209 if version == 1:
210 v1_config.append({"type": "manual"})
181 else:211 else:
182 for address in addresses:212 for address in addresses:
183 subnet = address.subnet213 subnet = address.subnet
184 if subnet is not None:214 if subnet is not None:
185 subnet_len = subnet.cidr.split('/')[1]215 subnet_len = subnet.cidr.split('/')[1]
186 subnet_operation = {216 cidr = "%s/%s" % (str(address.ip), subnet_len)
217 v1_subnet_operation = {
187 "type": "static",218 "type": "static",
188 "address": "%s/%s" % (str(address.ip), subnet_len)219 "address": cidr
189 }220 }
221 if address.ip is not None:
222 # If the address is None, that means we're generating a
223 # preseed for a Node that is not (or is no longer) in
224 # the READY state; so it might have auto-assigned IP
225 # addresses which have not yet been determined. It
226 # would be nice if there was a way to express this, if
227 # only for debugging purposes. For now, just ignore
228 # such addresses.
229 v1_subnet_operation['address'] = cidr
230 v2_cidrs.append(cidr)
231 if "addresses" not in v2_config:
232 v2_config["addresses"] = v2_cidrs
233 v1_config.append(v1_subnet_operation)
190 self.addr_family_present[234 self.addr_family_present[
191 IPAddress(address.ip).version] = True235 IPNetwork(subnet.cidr).version] = True
236 # The default gateway is set on the subnet operation for
237 # the v1 YAML, but it's per-interface for the v2 YAML.
192 self._set_default_gateway(238 self._set_default_gateway(
193 subnet, subnet_operation)239 subnet,
240 v1_subnet_operation if version == 1 else v2_config)
194 if subnet.dns_servers is not None:241 if subnet.dns_servers is not None:
195 subnet_operation["dns_nameservers"] = (242 v1_subnet_operation["dns_nameservers"] = (
196 subnet.dns_servers)243 subnet.dns_servers)
197 addrs.append(subnet_operation)244 if "nameservers" not in v2_config:
245 v2_config["nameservers"] = v2_nameservers
246 # XXX should also support search paths.
247 if "addresses" not in v2_nameservers:
248 v2_nameservers["addresses"] = []
249 v2_nameservers["addresses"].extend(
250 [server for server in subnet.dns_servers])
198 self.matching_routes.update(251 self.matching_routes.update(
199 self._get_matching_routes(subnet))252 self._get_matching_routes(subnet))
200 if dhcp_type:253 if dhcp_type:
201 addrs.append(254 v1_config.append(
202 {"type": dhcp_type}255 {"type": dhcp_type}
203 )256 )
204 return addrs257 if dhcp_type == "dhcp":
258 v2_config.update({
259 "dhcp4": True,
260 "dhcp6": True,
261 })
262 elif dhcp_type == "dhcp4":
263 v2_config.update({
264 "dhcp4": True,
265 })
266 elif dhcp_type == "dhcp6":
267 v2_config.update({
268 "dhcp6": True,
269 })
270 if version == 1:
271 return v1_config
272 elif version == 2:
273 return v2_config
205274
206 def _generate_vlan_operation(self):275 def _generate_vlan_operation(self, version=1):
207 """Generate vlan operation for `iface` and place in276 """Generate vlan operation for `iface` and place in
208 `network_config`."""277 `network_config`."""
209 vlan = self.iface.vlan278 vlan = self.iface.vlan
210 name = self.iface.get_name()279 name = self.name
211 addrs = self._generate_addresses()280 addrs = self._generate_addresses(version=version)
212 vlan_operation = self._get_initial_params()281 vlan_operation = self._get_initial_params()
213 vlan_operation.update({282 if version == 1:
214 "id": name,283 vlan_operation.update({
215 "type": "vlan",284 "id": name,
216 "name": name,285 "type": "vlan",
217 "vlan_link": self.iface.parents.first().get_name(),286 "name": name,
218 "vlan_id": vlan.vid,287 "vlan_link": self.iface.parents.first().get_name(),
219 })288 "vlan_id": vlan.vid,
220 if addrs:289 })
221 vlan_operation["subnets"] = addrs290 if addrs:
291 vlan_operation["subnets"] = addrs
292 elif version == 2:
293 vlan_operation.update({
294 "id": vlan.vid,
295 "link": self.iface.parents.first().get_name(),
296 })
297 vlan_operation.update(addrs)
222 return vlan_operation298 return vlan_operation
223299
224 def _generate_bond_operation(self):300 def _generate_bond_operation(self, version=1):
225 """Generate bond operation for `iface` and place in301 """Generate bond operation for `iface` and place in
226 `network_config`."""302 `network_config`."""
227 addrs = self._generate_addresses()303 addrs = self._generate_addresses(version=version)
228 bond_operation = self._get_initial_params()304 bond_operation = self._get_initial_params()
229 bond_operation.update({305 if version == 1:
230 "id": self.iface.get_name(),306 bond_operation.update({
231 "type": "bond",307 "id": self.name,
232 "name": self.iface.get_name(),308 "type": "bond",
233 "mac_address": str(self.iface.mac_address),309 "name": self.name,
234 "bond_interfaces": [parent.get_name() for parent in310 "mac_address": str(self.iface.mac_address),
235 self.iface.parents.order_by('name')],311 "bond_interfaces": [parent.get_name() for parent in
236 "params": self._get_bond_params(),312 self.iface.parents.order_by('name')],
237 })313 "params": self._get_bond_params(),
238 if addrs:314 })
239 bond_operation["subnets"] = addrs315 if addrs:
316 bond_operation["subnets"] = addrs
317 else:
318 bond_operation.update({
319 # XXX mpontillo 2017-02-17: netplan does not yet support
320 # specifying the MAC that should be used for a bond.
321 # See launchpad bug #1664698.
322 # "macaddress": str(self.iface.mac_address),
323 "interfaces": [
324 parent.get_name()
325 for parent in self.iface.parents.order_by('name')
326 ],
327 # XXX mpontillo 2017-02-17: netplan does not yet support
328 # specifying bond parameters. See launchpad bug #1664702.
329 # "params": self._get_bond_params(),
330 })
331 bond_operation.update(addrs)
240 return bond_operation332 return bond_operation
241333
242 def _generate_bridge_operation(self):334 def _generate_bridge_operation(self, version=1):
243 """Generate bridge operation for this interface."""335 """Generate bridge operation for this interface."""
244 addrs = self._generate_addresses()336 addrs = self._generate_addresses(version=version)
245 bridge_operation = self._get_initial_params()337 bridge_operation = self._get_initial_params()
246 bridge_operation.update({338 if version == 1:
247 "id": self.iface.get_name(),339 bridge_operation.update({
248 "type": "bridge",340 "id": self.name,
249 "name": self.iface.get_name(),341 "type": "bridge",
250 "mac_address": str(self.iface.mac_address),342 "name": self.name,
251 "bridge_interfaces": [parent.get_name() for parent in343 "mac_address": str(self.iface.mac_address),
252 self.iface.parents.order_by('name')],344 "bridge_interfaces": [parent.get_name() for parent in
253 "params": self._get_bridge_params(),345 self.iface.parents.order_by('name')],
254 })346 "params": self._get_bridge_params(),
255 if addrs:347 })
256 bridge_operation["subnets"] = addrs348 if addrs:
349 bridge_operation["subnets"] = addrs
350 elif version == 2:
351 bridge_operation.update({
352 # XXX mpontillo 2017-02-17: netplan does not yet support
353 # specifying the MAC that should be used for a bond.
354 # See launchpad bug #1664698.
355 # "macaddress": str(self.iface.mac_address),
356 "interfaces": [
357 parent.get_name()
358 for parent in self.iface.parents.order_by('name')
359 ],
360 # XXX mpontillo 2017-02-17: netplan does not yet support
361 # specifying bridge parameters. See launchpad bug #1664702.
362 # "params": self._get_bridge_params(),
363 })
364 bridge_operation.update(addrs)
257 return bridge_operation365 return bridge_operation
258366
259 def _get_initial_params(self):367 def _get_initial_params(self):
@@ -299,13 +407,20 @@
299class NodeNetworkConfiguration:407class NodeNetworkConfiguration:
300 """Generator for the YAML network configuration for curtin."""408 """Generator for the YAML network configuration for curtin."""
301409
302 def __init__(self, node):410 def __init__(self, node, version=1):
303 """Create the YAML network configuration for the specified node, and411 """Create the YAML network configuration for the specified node, and
304 store it in the `config` ivar.412 store it in the `config` ivar.
305 """413 """
306 self.node = node414 self.node = node
307 self.matching_routes = set()415 self.matching_routes = set()
308 self.network_config = []416 self.v1_config = []
417 self.v2_config = {
418 "version": 2
419 }
420 self.v2_ethernets = {}
421 self.v2_vlans = {}
422 self.v2_bonds = {}
423 self.v2_bridges = {}
309 self.gateway_ipv4_set = False424 self.gateway_ipv4_set = False
310 self.gateway_ipv6_set = False425 self.gateway_ipv6_set = False
311 # The default value is False: expected keys are 4 and 6.426 # The default value is False: expected keys are 4 and 6.
@@ -318,13 +433,21 @@
318 for iface in interfaces:433 for iface in interfaces:
319 if not iface.is_enabled():434 if not iface.is_enabled():
320 continue435 continue
321 generator = InterfaceConfiguration(iface, self)436 generator = InterfaceConfiguration(iface, self, version=version)
322 self.matching_routes.update(generator.matching_routes)437 self.matching_routes.update(generator.matching_routes)
323 self.addr_family_present.update(generator.addr_family_present)438 self.addr_family_present.update(generator.addr_family_present)
324 self.network_config.append(generator.config)439 if version == 1:
325440 self.v1_config.append(generator.config)
326 # Generate each YAML operation in the network_config.441 elif version == 2:
327 self._generate_route_operations()442 v2_config = {generator.name: generator.config}
443 if generator.type == INTERFACE_TYPE.PHYSICAL:
444 self.v2_ethernets.update(v2_config)
445 elif generator.type == INTERFACE_TYPE.VLAN:
446 self.v2_vlans.update(v2_config)
447 elif generator.type == INTERFACE_TYPE.BOND:
448 self.v2_bonds.update(v2_config)
449 elif generator.type == INTERFACE_TYPE.BRIDGE:
450 self.v2_bridges.update(v2_config)
328451
329 # If we have no IPv6 addresses present, make sure we claim IPv4, so452 # If we have no IPv6 addresses present, make sure we claim IPv4, so
330 # that we at least get some address.453 # that we at least get some address.
@@ -336,31 +459,65 @@
336 name459 name
337 for name in sorted(get_dns_search_paths())460 for name in sorted(get_dns_search_paths())
338 if name != self.node.domain.name]461 if name != self.node.domain.name]
339 self.network_config.append({462 self._generate_route_operations(version=version)
463 self.v1_config.append({
340 "type": "nameserver",464 "type": "nameserver",
341 "address": default_dns_servers,465 "address": default_dns_servers,
342 "search": search_list,466 "search": search_list,
343 })467 })
344468 if version == 1:
345 network_config = {469 network_config = {
346 "network_commands": {470 "network": {
347 "builtin": ["curtin", "net-meta", "custom"],471 "version": 1,
348 },472 "config": self.v1_config,
349 "network": {473 },
350 "version": 1,474 }
351 "config": self.network_config,475 else:
352 },476 network_config = {
353 }477 "network": self.v2_config,
354 # Render the resulting YAML.478 }
355 self.config = yaml.safe_dump(network_config, default_flow_style=False)479 v2_config = network_config['network']
356480 if len(self.v2_ethernets) > 0:
357 def _generate_route_operations(self):481 v2_config.update({"ethernets": self.v2_ethernets})
482 if len(self.v2_vlans) > 0:
483 v2_config.update({"vlans": self.v2_vlans})
484 if len(self.v2_bonds) > 0:
485 v2_config.update({"bonds": self.v2_bonds})
486 if len(self.v2_bridges) > 0:
487 v2_config.update({"bridges": self.v2_bridges})
488 # XXX mpontillo 2017-02-17: netplan has no concept of "default"
489 # DNS servers. Need to define how to convey this.
490 # See launchpad bug #1664806.
491 # if len(default_dns_servers) > 0 or len(search_list) > 0:
492 # nameservers = {}
493 # if len(search_list) > 0:
494 # nameservers.update({"search": search_list})
495 # if len(default_dns_servers) > 0:
496 # nameservers.update({"addresses": default_dns_servers})
497 # v2_config.update({"nameservers": nameservers})
498 self.config = network_config
499
500 def _generate_route_operations(self, version=1):
358 """Generate all route operations."""501 """Generate all route operations."""
502 routes = []
359 for route in sorted(self.matching_routes, key=attrgetter("id")):503 for route in sorted(self.matching_routes, key=attrgetter("id")):
360 self.network_config.append(_generate_route_operation(route))504 routes.append(_generate_route_operation(route, version=version))
361505 if version == 1:
362506 self.v1_config.extend(routes)
363def compose_curtin_network_config(node):507 elif version == 2 and len(routes) > 0:
508 self.v2_config["routes"] = routes
509
510
511def compose_curtin_network_config(node, version=1):
364 """Compose the network configuration for curtin."""512 """Compose the network configuration for curtin."""
365 generator = NodeNetworkConfiguration(node)513 generator = NodeNetworkConfiguration(node, version=version)
366 return [generator.config]514 curtin_config = {
515 "network_commands": {
516 "builtin": ["curtin", "net-meta", "custom"],
517 }
518 }
519 curtin_config.update(generator.config)
520 # Render the resulting YAML.
521 curtin_config_yaml = yaml.safe_dump(
522 curtin_config, default_flow_style=False)
523 return [curtin_config_yaml]
367524
=== modified file 'src/maasserver/tests/test_preseed_network.py'
--- src/maasserver/tests/test_preseed_network.py 2017-02-22 14:55:09 +0000
+++ src/maasserver/tests/test_preseed_network.py 2017-03-06 11:48:26 +0000
@@ -16,7 +16,10 @@
16 IPADDRESS_TYPE,16 IPADDRESS_TYPE,
17)17)
18from maasserver.models.staticroute import StaticRoute18from maasserver.models.staticroute import StaticRoute
19from maasserver.preseed_network import compose_curtin_network_config19from maasserver.preseed_network import (
20 compose_curtin_network_config,
21 NodeNetworkConfiguration,
22)
20from maasserver.testing.factory import factory23from maasserver.testing.factory import factory
21from maasserver.testing.testcase import MAASServerTestCase24from maasserver.testing.testcase import MAASServerTestCase
22from netaddr import IPNetwork25from netaddr import IPNetwork
@@ -461,3 +464,168 @@
461 net_config += self.collectDNSConfig(node)464 net_config += self.collectDNSConfig(node)
462 config = compose_curtin_network_config(node)465 config = compose_curtin_network_config(node)
463 self.assertNetworkConfig(net_config, config)466 self.assertNetworkConfig(net_config, config)
467
468
469class TestNetplan(MAASServerTestCase):
470
471 def _render_netplan_dict(self, node):
472 return NodeNetworkConfiguration(node, version=2).config
473
474 def test__single_ethernet_interface(self):
475 node = factory.make_Node()
476 factory.make_Interface(
477 node=node, name='eth0', mac_address="00:01:02:03:04:05")
478 netplan = self._render_netplan_dict(node)
479 expected_netplan = {
480 'network': {
481 'version': 2,
482 'ethernets': {
483 'eth0': {
484 'match': {'macaddress': '00:01:02:03:04:05'},
485 'mtu': 1500,
486 'set-name': 'eth0'
487 },
488 },
489 }
490 }
491 self.expectThat(netplan, Equals(expected_netplan))
492
493 def test__multiple_ethernet_interfaces(self):
494 node = factory.make_Node()
495 factory.make_Interface(
496 node=node, name='eth0', mac_address="00:01:02:03:04:05")
497 factory.make_Interface(
498 node=node, name='eth1', mac_address="02:01:02:03:04:05")
499 netplan = self._render_netplan_dict(node)
500 expected_netplan = {
501 'network': {
502 'version': 2,
503 'ethernets': {
504 'eth0': {
505 'match': {'macaddress': '00:01:02:03:04:05'},
506 'mtu': 1500,
507 'set-name': 'eth0'
508 },
509 'eth1': {
510 'match': {'macaddress': '02:01:02:03:04:05'},
511 'mtu': 1500,
512 'set-name': 'eth1'
513 },
514 },
515 },
516 }
517 self.expectThat(netplan, Equals(expected_netplan))
518
519 def test__bond(self):
520 node = factory.make_Node()
521 eth0 = factory.make_Interface(
522 node=node, name='eth0', mac_address="00:01:02:03:04:05")
523 eth1 = factory.make_Interface(
524 node=node, name='eth1', mac_address="02:01:02:03:04:05")
525 factory.make_Interface(
526 INTERFACE_TYPE.BOND,
527 node=node, name='bond0', parents=[eth0, eth1])
528 netplan = self._render_netplan_dict(node)
529 expected_netplan = {
530 'network': {
531 'version': 2,
532 'ethernets': {
533 'eth0': {
534 'match': {'macaddress': '00:01:02:03:04:05'},
535 'mtu': 1500,
536 'set-name': 'eth0'
537 },
538 'eth1': {
539 'match': {'macaddress': '02:01:02:03:04:05'},
540 'mtu': 1500,
541 'set-name': 'eth1'
542 },
543 },
544 'bonds': {
545 'bond0': {
546 'interfaces': ['eth0', 'eth1'],
547 'mtu': 1500
548 },
549 },
550 }
551 }
552 self.expectThat(netplan, Equals(expected_netplan))
553
554 def test__bridge(self):
555 node = factory.make_Node()
556 eth0 = factory.make_Interface(
557 node=node, name='eth0', mac_address="00:01:02:03:04:05")
558 eth1 = factory.make_Interface(
559 node=node, name='eth1', mac_address="02:01:02:03:04:05")
560 factory.make_Interface(
561 INTERFACE_TYPE.BRIDGE,
562 node=node, name='br0', parents=[eth0, eth1])
563 netplan = self._render_netplan_dict(node)
564 expected_netplan = {
565 'network': {
566 'version': 2,
567 'ethernets': {
568 'eth0': {
569 'match': {'macaddress': '00:01:02:03:04:05'},
570 'mtu': 1500,
571 'set-name': 'eth0'
572 },
573 'eth1': {
574 'match': {'macaddress': '02:01:02:03:04:05'},
575 'mtu': 1500,
576 'set-name': 'eth1'
577 },
578 },
579 'bridges': {
580 'br0': {
581 'interfaces': ['eth0', 'eth1'],
582 'mtu': 1500
583 },
584 },
585 }
586 }
587 self.expectThat(netplan, Equals(expected_netplan))
588
589 def test__bridged_bond(self):
590 node = factory.make_Node()
591 eth0 = factory.make_Interface(
592 node=node, name='eth0', mac_address="00:01:02:03:04:05")
593 eth1 = factory.make_Interface(
594 node=node, name='eth1', mac_address="02:01:02:03:04:05")
595 bond0 = factory.make_Interface(
596 INTERFACE_TYPE.BOND,
597 node=node, name='bond0', parents=[eth0, eth1])
598 factory.make_Interface(
599 INTERFACE_TYPE.BRIDGE,
600 node=node, name='br0', parents=[bond0])
601 netplan = self._render_netplan_dict(node)
602 expected_netplan = {
603 'network': {
604 'version': 2,
605 'ethernets': {
606 'eth0': {
607 'match': {'macaddress': '00:01:02:03:04:05'},
608 'mtu': 1500,
609 'set-name': 'eth0'
610 },
611 'eth1': {
612 'match': {'macaddress': '02:01:02:03:04:05'},
613 'mtu': 1500,
614 'set-name': 'eth1'
615 },
616 },
617 'bonds': {
618 'bond0': {
619 'interfaces': ['eth0', 'eth1'],
620 'mtu': 1500
621 },
622 },
623 'bridges': {
624 'br0': {
625 'interfaces': ['bond0'],
626 'mtu': 1500
627 },
628 },
629 }
630 }
631 self.expectThat(netplan, Equals(expected_netplan))