Merge lp:~jtv/maas/generate-node-networking-config-2 into lp:~maas-committers/maas/trunk
- generate-node-networking-config-2
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | no longer in the source branch. |
Merged at revision: | 3069 |
Proposed branch: | lp:~jtv/maas/generate-node-networking-config-2 |
Merge into: | lp:~maas-committers/maas/trunk |
Prerequisite: | lp:~jtv/maas/generate-node-networking-config-1 |
Diff against target: |
452 lines (+347/-7) 2 files modified
src/maasserver/networking_preseed.py (+122/-2) src/maasserver/tests/test_networking_preseed.py (+225/-5) |
To merge this branch: | bzr merge lp:~jtv/maas/generate-node-networking-config-2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | Approve | ||
Review via email: mp+234457@code.launchpad.net |
Commit message
More curtin networking config: generate DNS server entries, and network entries. (This doesn't include the network entries into the config yet; that's for a later branch.) Also, update to a seemingly newer example of the file format.
Description of the change
This builds on a predecessor branch. You may want to have a look at that first.
Jeroen
Graham Binns (gmb) wrote : | # |
Graham Binns (gmb) : | # |
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~jtv/maas/generate-node-networking-config-2 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Hit http://
Hit http://
Ign http://
Ign http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === modified file 'src/maasserver/networking_preseed.py' |
2 | --- src/maasserver/networking_preseed.py 2014-09-24 02:48:44 +0000 |
3 | +++ src/maasserver/networking_preseed.py 2014-09-24 03:07:38 +0000 |
4 | @@ -7,6 +7,10 @@ |
5 | |
6 | https://gist.github.com/jayofdoom/b035067523defec7fb53 |
7 | |
8 | +A different version of the format is documented here: |
9 | + |
10 | + http://bit.ly/1uqWvC8 |
11 | + |
12 | The installer running on a node will use this to set up the node's networking |
13 | according to MAAS's specifications. |
14 | """ |
15 | @@ -25,7 +29,13 @@ |
16 | ] |
17 | |
18 | from lxml import etree |
19 | +from maasserver.dns.zonegenerator import get_dns_server_address |
20 | +from maasserver.exceptions import UnresolvableHost |
21 | from maasserver.models.nodeprobeddetails import get_probed_details |
22 | +from netaddr import ( |
23 | + IPAddress, |
24 | + IPNetwork, |
25 | + ) |
26 | |
27 | |
28 | def extract_network_interface_data(element): |
29 | @@ -90,11 +100,120 @@ |
30 | """ |
31 | return { |
32 | 'id': interface, |
33 | - 'type': 'ethernet', |
34 | + 'type': 'phy', |
35 | 'ethernet_mac_address': mac, |
36 | } |
37 | |
38 | |
39 | +def generate_dns_server_entry(dns_address): |
40 | + """Generate the `services` list entry for the given DNS server. |
41 | + |
42 | + :param dns_address: IP address for a DNS server, in text form. |
43 | + :return: A dict specifying the DNS server as a network service, with |
44 | + keys `address` and `type` (and possibly more). |
45 | + """ |
46 | + return { |
47 | + 'type': 'dns', |
48 | + 'address': dns_address, |
49 | + } |
50 | + |
51 | + |
52 | +def list_dns_servers(node): |
53 | + """Return DNS servers, IPv4 and IPv6 as appropriate, for use by `node`. |
54 | + |
55 | + These are always the MAAS-controlled DNS servers. |
56 | + """ |
57 | + cluster = node.nodegroup |
58 | + servers = [] |
59 | + if not node.disable_ipv4: |
60 | + try: |
61 | + servers.append( |
62 | + get_dns_server_address(cluster, ipv4=True, ipv6=False)) |
63 | + except UnresolvableHost: |
64 | + # No IPv4 DNS server. |
65 | + pass |
66 | + try: |
67 | + servers.append(get_dns_server_address(cluster, ipv4=False, ipv6=True)) |
68 | + except UnresolvableHost: |
69 | + # No IPv6 DNS server. |
70 | + pass |
71 | + return [dns_server for dns_server in servers if dns_server is not None] |
72 | + |
73 | + |
74 | +def generate_route_entries(cluster_interface): |
75 | + """Generate `routes` list entries for a cluster interface. |
76 | + |
77 | + Actually this returns exactly one route (the default route) if |
78 | + `cluster_interface` has a router set; or none otherwise. |
79 | + """ |
80 | + if cluster_interface.router_ip in ('', None): |
81 | + # No routes available. |
82 | + return [] |
83 | + elif IPAddress(cluster_interface.ip).version == 4: |
84 | + return [ |
85 | + { |
86 | + 'network': '0.0.0.0', |
87 | + 'netmask': '0.0.0.0', |
88 | + 'gateway': unicode(cluster_interface.router_ip), |
89 | + }, |
90 | + ] |
91 | + else: |
92 | + return [ |
93 | + { |
94 | + 'network': '::', |
95 | + 'netmask': '::', |
96 | + 'gateway': unicode(cluster_interface.router_ip), |
97 | + }, |
98 | + ] |
99 | + |
100 | + |
101 | +def generate_network_entry(network_interface, cluster_interface, ip=None): |
102 | + """Generate the `networks` list entry for the given network connection. |
103 | + |
104 | + :param network_interface: Name of the network interface (on the node) that |
105 | + connects to this network. |
106 | + :param cluster_interface: The `NodeGroupInterface` responsible for this |
107 | + network. (Do not confuse its `interface` property, which is a network |
108 | + interface on the cluster controller, with the `network_interface` |
109 | + parameter which is a network interface on the node.) |
110 | + :param ip: Optional IP address. If not given, use DHCP. |
111 | + """ |
112 | + network_types = { |
113 | + 4: 'ipv4', |
114 | + 6: 'ipv6', |
115 | + } |
116 | + network = cluster_interface.network |
117 | + |
118 | + # Still lacking a few entries that we don't have enough information about: |
119 | + # * id -- does this need to match anything anywhere? |
120 | + # * network_id -- what is this, and how do we compose it? |
121 | + # |
122 | + # It's tempting to use cluster_interface.name for the 'id,' but that |
123 | + # could be confusing: it was probably generated based on the name of its |
124 | + # network interface on the cluster. Which will probably often match the |
125 | + # name of the node cluster interface, but is completely unrelated to it. |
126 | + entry = { |
127 | + 'link': network_interface, |
128 | + # The example we have does not show IPv6 netmasks. Should we pass |
129 | + # width in bits? |
130 | + 'type': network_types[network.version], |
131 | + 'routes': generate_route_entries(cluster_interface) |
132 | + } |
133 | + if ip is not None: |
134 | + # Set static IP address. |
135 | + # How do we tell the node to request a dynamic IP address over DHCP? |
136 | + # Is just omitting ip_address the appropriate behaviour? |
137 | + if network.version == 4: |
138 | + entry['ip_address'] = ip |
139 | + else: |
140 | + # Include network size directly in IPv6 address. |
141 | + entry['ip_address'] = unicode( |
142 | + IPNetwork("%s/%s" % (ip, network.netmask))) |
143 | + if network.version == 4: |
144 | + entry['netmask'] = unicode(network.netmask) |
145 | + return entry |
146 | + |
147 | + |
148 | def generate_networking_config(node): |
149 | """Generate a networking preseed for `node`. |
150 | |
151 | @@ -108,7 +227,8 @@ |
152 | 'provider': "MAAS", |
153 | 'network_info': { |
154 | 'services': [ |
155 | - # List DNS servers here. |
156 | + generate_dns_server_entry(dns_server) |
157 | + for dns_server in list_dns_servers(node) |
158 | ], |
159 | 'networks': [ |
160 | # Write network specs here. |
161 | |
162 | === modified file 'src/maasserver/tests/test_networking_preseed.py' |
163 | --- src/maasserver/tests/test_networking_preseed.py 2014-09-24 02:47:58 +0000 |
164 | +++ src/maasserver/tests/test_networking_preseed.py 2014-09-24 03:07:38 +0000 |
165 | @@ -15,16 +15,26 @@ |
166 | __all__ = [ |
167 | ] |
168 | |
169 | +from random import randint |
170 | + |
171 | from maasserver import networking_preseed |
172 | +from maasserver.dns import zonegenerator |
173 | +from maasserver.enum import NODEGROUPINTERFACE_MANAGEMENT |
174 | +from maasserver.exceptions import UnresolvableHost |
175 | from maasserver.networking_preseed import ( |
176 | extract_network_interfaces, |
177 | + generate_dns_server_entry, |
178 | generate_ethernet_link_entry, |
179 | + generate_network_entry, |
180 | generate_networking_config, |
181 | + generate_route_entries, |
182 | + list_dns_servers, |
183 | normalise_mac, |
184 | ) |
185 | from maasserver.testing.factory import factory |
186 | from maasserver.testing.testcase import MAASServerTestCase |
187 | from maastesting.matchers import MockCalledOnceWith |
188 | +from testtools.matchers import HasLength |
189 | |
190 | |
191 | def make_denormalised_mac(): |
192 | @@ -211,12 +221,210 @@ |
193 | self.assertEqual( |
194 | { |
195 | 'id': interface, |
196 | - 'type': 'ethernet', |
197 | + 'type': 'phy', |
198 | 'ethernet_mac_address': mac, |
199 | }, |
200 | generate_ethernet_link_entry(interface, mac)) |
201 | |
202 | |
203 | +class TestGenerateDNServerEntry(MAASServerTestCase): |
204 | + |
205 | + def test__returns_dict(self): |
206 | + address = factory.make_ipv4_address() |
207 | + self.assertEqual( |
208 | + { |
209 | + 'type': 'dns', |
210 | + 'address': address, |
211 | + }, |
212 | + generate_dns_server_entry(address)) |
213 | + |
214 | + |
215 | +def patch_dns_servers(testcase, ipv4_dns=None, ipv6_dns=None): |
216 | + """Patch `get_dns_server_address` to return the given addresses. |
217 | + |
218 | + The fake will return `ipv4_dns` or `ipv6_dns` as appropriate to the |
219 | + arguments. For that reason, this patch does not use a `Mock`. |
220 | + """ |
221 | + |
222 | + def fake_get_maas_facing_server_address(cluster, ipv4=True, ipv6=True): |
223 | + result = None |
224 | + if ipv4: |
225 | + result = ipv4_dns |
226 | + if result is None and ipv6: |
227 | + result = ipv6_dns |
228 | + if result is None: |
229 | + raise UnresolvableHost() |
230 | + return result |
231 | + |
232 | + testcase.patch( |
233 | + zonegenerator, 'get_maas_facing_server_address', |
234 | + fake_get_maas_facing_server_address) |
235 | + testcase.patch(zonegenerator, 'warn_loopback') |
236 | + |
237 | + |
238 | +class ListDNSServers(MAASServerTestCase): |
239 | + |
240 | + def test__includes_ipv4_and_ipv6_by_default(self): |
241 | + ipv4_dns = factory.make_ipv4_address() |
242 | + ipv6_dns = factory.make_ipv6_address() |
243 | + patch_dns_servers(self, ipv4_dns=ipv4_dns, ipv6_dns=ipv6_dns) |
244 | + node = factory.make_Node(disable_ipv4=False) |
245 | + self.assertItemsEqual([ipv4_dns, ipv6_dns], list_dns_servers(node)) |
246 | + |
247 | + def test__omits_ipv4_if_disabled_for_node(self): |
248 | + ipv4_dns = factory.make_ipv4_address() |
249 | + ipv6_dns = factory.make_ipv6_address() |
250 | + patch_dns_servers(self, ipv4_dns=ipv4_dns, ipv6_dns=ipv6_dns) |
251 | + node = factory.make_Node(disable_ipv4=True) |
252 | + self.assertItemsEqual([ipv6_dns], list_dns_servers(node)) |
253 | + |
254 | + def test__omits_ipv4_if_unvailable(self): |
255 | + ipv6_dns = factory.make_ipv6_address() |
256 | + patch_dns_servers(self, ipv6_dns=ipv6_dns) |
257 | + node = factory.make_Node(disable_ipv4=False) |
258 | + self.assertItemsEqual([ipv6_dns], list_dns_servers(node)) |
259 | + |
260 | + def test__omits_ipv6_if_unavailable(self): |
261 | + ipv4_dns = factory.make_ipv4_address() |
262 | + patch_dns_servers(self, ipv4_dns=ipv4_dns) |
263 | + node = factory.make_Node(disable_ipv4=False) |
264 | + self.assertItemsEqual([ipv4_dns], list_dns_servers(node)) |
265 | + |
266 | + |
267 | +def make_cluster_interface(network=None, **kwargs): |
268 | + return factory.make_NodeGroupInterface( |
269 | + factory.make_NodeGroup(), network=network, **kwargs) |
270 | + |
271 | + |
272 | +class TestGenerateRouteEntries(MAASServerTestCase): |
273 | + |
274 | + def test__generates_IPv4_default_route_if_available(self): |
275 | + network = factory.make_ipv4_network() |
276 | + router = factory.pick_ip_in_network(network) |
277 | + cluster_interface = make_cluster_interface(network, router_ip=router) |
278 | + self.assertEqual( |
279 | + [ |
280 | + { |
281 | + 'network': '0.0.0.0', |
282 | + 'netmask': '0.0.0.0', |
283 | + 'gateway': unicode(router), |
284 | + }, |
285 | + ], |
286 | + generate_route_entries(cluster_interface)) |
287 | + |
288 | + def test__generates_IPv6_default_route_if_available(self): |
289 | + network = factory.make_ipv6_network() |
290 | + router = factory.pick_ip_in_network(network) |
291 | + cluster_interface = make_cluster_interface(network, router_ip=router) |
292 | + self.assertEqual( |
293 | + [ |
294 | + { |
295 | + 'network': '::', |
296 | + 'netmask': '::', |
297 | + 'gateway': unicode(router), |
298 | + }, |
299 | + ], |
300 | + generate_route_entries(cluster_interface)) |
301 | + |
302 | + def test__generates_empty_list_if_no_route_available(self): |
303 | + network = factory.make_ipv4_network() |
304 | + cluster_interface = make_cluster_interface( |
305 | + network, management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED, |
306 | + router_ip='') |
307 | + self.assertEqual([], generate_route_entries(cluster_interface)) |
308 | + |
309 | + |
310 | +class TestGenerateNetworkEntry(MAASServerTestCase): |
311 | + |
312 | + def test__generates_IPv4_dict(self): |
313 | + network = factory.make_ipv4_network() |
314 | + network_interface = factory.make_name('eth') |
315 | + cluster_interface = make_cluster_interface(network) |
316 | + ip = factory.pick_ip_in_network(network) |
317 | + |
318 | + entry = generate_network_entry( |
319 | + network_interface, cluster_interface, ip=ip) |
320 | + |
321 | + del entry['routes'] |
322 | + self.assertEqual( |
323 | + { |
324 | + 'type': 'ipv4', |
325 | + 'link': network_interface, |
326 | + 'ip_address': unicode(ip), |
327 | + 'netmask': unicode(network.netmask), |
328 | + }, |
329 | + entry) |
330 | + |
331 | + def test__generates_IPv6_dict(self): |
332 | + slash = randint(48, 64) |
333 | + network = factory.make_ipv6_network(slash=slash) |
334 | + network_interface = factory.make_name('eth') |
335 | + cluster_interface = make_cluster_interface(network) |
336 | + ip = factory.pick_ip_in_network(network) |
337 | + |
338 | + entry = generate_network_entry( |
339 | + network_interface, cluster_interface, ip=ip) |
340 | + |
341 | + del entry['routes'] |
342 | + self.assertEqual( |
343 | + { |
344 | + 'type': 'ipv6', |
345 | + 'link': network_interface, |
346 | + 'ip_address': '%s/%d' % (ip, slash), |
347 | + }, |
348 | + entry) |
349 | + |
350 | + def test__omits_IP_if_not_given(self): |
351 | + network = factory.make_ipv4_network() |
352 | + network_interface = factory.make_name('eth') |
353 | + cluster_interface = make_cluster_interface(network) |
354 | + |
355 | + entry = generate_network_entry(network_interface, cluster_interface) |
356 | + |
357 | + del entry['routes'] |
358 | + self.assertEqual( |
359 | + { |
360 | + 'type': 'ipv4', |
361 | + 'link': network_interface, |
362 | + 'netmask': unicode(network.netmask), |
363 | + }, |
364 | + entry) |
365 | + |
366 | + def test__tells_IPv4_from_IPv6_even_without_IP(self): |
367 | + cluster_interface = make_cluster_interface(factory.make_ipv6_network()) |
368 | + entry = generate_network_entry( |
369 | + factory.make_name('eth'), cluster_interface) |
370 | + self.assertEqual('ipv6', entry['type']) |
371 | + |
372 | + def test__includes_IPv4_routes_on_IPv4_network(self): |
373 | + network = factory.make_ipv4_network() |
374 | + router = factory.pick_ip_in_network(network) |
375 | + cluster_interface = make_cluster_interface( |
376 | + network, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP, |
377 | + router_ip=router) |
378 | + |
379 | + entry = generate_network_entry( |
380 | + factory.make_name('eth'), cluster_interface) |
381 | + |
382 | + self.assertThat(entry['routes'], HasLength(1)) |
383 | + [route] = entry['routes'] |
384 | + self.assertEqual(unicode(router), route['gateway']) |
385 | + |
386 | + def test__includes_IPv6_routes_on_IPv6_network(self): |
387 | + network = factory.make_ipv6_network() |
388 | + router = factory.pick_ip_in_network(network) |
389 | + cluster_interface = make_cluster_interface( |
390 | + network, management=NODEGROUPINTERFACE_MANAGEMENT.DHCP, |
391 | + router_ip=router) |
392 | + |
393 | + entry = generate_network_entry( |
394 | + factory.make_name('eth'), cluster_interface) |
395 | + |
396 | + self.assertThat(entry['routes'], HasLength(1)) |
397 | + [route] = entry['routes'] |
398 | + self.assertEqual(unicode(router), route['gateway']) |
399 | + |
400 | + |
401 | class TestGenerateNetworkingConfig(MAASServerTestCase): |
402 | |
403 | def patch_interfaces(self, interface_mac_pairs): |
404 | @@ -227,11 +435,13 @@ |
405 | |
406 | def test__returns_config_dict(self): |
407 | self.patch_interfaces([]) |
408 | + patch_dns_servers(self) |
409 | config = generate_networking_config(factory.make_Node()) |
410 | self.assertIsInstance(config, dict) |
411 | self.assertEqual("MAAS", config['provider']) |
412 | |
413 | def test__includes_links(self): |
414 | + patch_dns_servers(self) |
415 | node = factory.make_Node() |
416 | interface = factory.make_name('eth') |
417 | mac = factory.make_mac_address() |
418 | @@ -244,7 +454,7 @@ |
419 | [ |
420 | { |
421 | 'id': interface, |
422 | - 'type': 'ethernet', |
423 | + 'type': 'phy', |
424 | 'ethernet_mac_address': mac, |
425 | }, |
426 | ], |
427 | @@ -252,12 +462,22 @@ |
428 | |
429 | def test__includes_networks(self): |
430 | # This section is not yet implemented, so expect an empty list. |
431 | + patch_dns_servers(self) |
432 | self.patch_interfaces([]) |
433 | config = generate_networking_config(factory.make_Node()) |
434 | self.assertEqual([], config['network_info']['networks']) |
435 | |
436 | def test__includes_dns_servers(self): |
437 | - # This section is not yet implemented, so expect an empty list. |
438 | + dns_address = factory.make_ipv4_address() |
439 | + patch_dns_servers(self, dns_address) |
440 | self.patch_interfaces([]) |
441 | - config = generate_networking_config(factory.make_Node()) |
442 | - self.assertEqual([], config['network_info']['services']) |
443 | + config = generate_networking_config( |
444 | + factory.make_Node(disable_ipv4=False)) |
445 | + self.assertEqual( |
446 | + [ |
447 | + { |
448 | + 'type': 'dns', |
449 | + 'address': dns_address, |
450 | + }, |
451 | + ], |
452 | + config['network_info']['services']) |
Great work, Jeroen. Thank you for persevering with this!
Just a few nitpicks; nothing major.