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