Merge lp:~jtv/maas/generate-node-networking-config-1 into lp:~maas-committers/maas/trunk
- generate-node-networking-config-1
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Jeroen T. Vermeulen |
Approved revision: | no longer in the source branch. |
Merged at revision: | 3068 |
Proposed branch: | lp:~jtv/maas/generate-node-networking-config-1 |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
393 lines (+384/-0) 2 files modified
src/maasserver/networking_preseed.py (+121/-0) src/maasserver/tests/test_networking_preseed.py (+263/-0) |
To merge this branch: | bzr merge lp:~jtv/maas/generate-node-networking-config-1 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Graham Binns (community) | Approve | ||
Review via email: mp+234413@code.launchpad.net |
Commit message
First part: generate a curtin network configuration preseed along the lines of the example that Scott provided.
Description of the change
We'll be able to send this into Curtin (how exactly I'm hoping to hear from Scott tonight) and it will configure the node's networking for us, once implementation in cloud-init is complete. According to Scott this is the only right way to configure the networking, so it's worth the extra work.
Jeroen
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~jtv/maas/generate-node-networking-config-1 into lp:maas failed. Below is the output from the failed tests.
Ign http://
Get:1 http://
Get:2 http://
Ign http://
Ign http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Get:5 http://
Hit http://
Get:6 http://
Hit http://
Get:7 http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:8 http://
Hit http://
Hit http://
Ign http://
Ign http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Fetched 1,101 kB in 0s (1,753 kB/s)
Reading package lists...
sudo DEBIAN_
--
Preview Diff
1 | === added file 'src/maasserver/networking_preseed.py' |
2 | --- src/maasserver/networking_preseed.py 1970-01-01 00:00:00 +0000 |
3 | +++ src/maasserver/networking_preseed.py 2014-09-24 02:49:10 +0000 |
4 | @@ -0,0 +1,121 @@ |
5 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
6 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
7 | + |
8 | +"""Network configuration preseed code. |
9 | + |
10 | +This will eventually generate installer networking configuration like: |
11 | + |
12 | + https://gist.github.com/jayofdoom/b035067523defec7fb53 |
13 | + |
14 | +The installer running on a node will use this to set up the node's networking |
15 | +according to MAAS's specifications. |
16 | +""" |
17 | + |
18 | +from __future__ import ( |
19 | + absolute_import, |
20 | + print_function, |
21 | + unicode_literals, |
22 | + ) |
23 | + |
24 | +str = None |
25 | + |
26 | +__metaclass__ = type |
27 | +__all__ = [ |
28 | + 'generate_networking_config', |
29 | + ] |
30 | + |
31 | +from lxml import etree |
32 | +from maasserver.models.nodeprobeddetails import get_probed_details |
33 | + |
34 | + |
35 | +def extract_network_interface_data(element): |
36 | + """Extract network interface name and MAC address from XML element. |
37 | + |
38 | + :return: A tuple of network interface name and MAC address as found in |
39 | + the XML. If either is not found, it will be `None`. |
40 | + """ |
41 | + interfaces = element.xpath("logicalname") |
42 | + macs = element.xpath("serial") |
43 | + if len(interfaces) == 0 or len(macs) == 0: |
44 | + # Not enough data. |
45 | + return None, None |
46 | + assert len(interfaces) == 1 |
47 | + assert len(macs) == 1 |
48 | + return interfaces[0].text, macs[0].text |
49 | + |
50 | + |
51 | +def normalise_mac(mac): |
52 | + """Return a MAC's normalised representation. |
53 | + |
54 | + This doesn't actually parse all the different formats for writing MAC |
55 | + addresses, but it does eliminate case differences. In practice, any MAC |
56 | + address this code is likely to will equal another version of itself after |
57 | + normalisation, even if they were originally different spellings. |
58 | + """ |
59 | + return mac.strip().lower() |
60 | + |
61 | + |
62 | +def extract_network_interfaces(node): |
63 | + """Extract network interfaces from node's `lshw` output. |
64 | + |
65 | + :param node: A `Node`. |
66 | + :return: A list of tuples describing the network interfaces. |
67 | + Each tuple consists of an interface name and a MAC address. |
68 | + """ |
69 | + node_details = get_probed_details([node.system_id]) |
70 | + if node.system_id not in node_details: |
71 | + return [] |
72 | + lshw_xml = node_details[node.system_id].get('lshw') |
73 | + if lshw_xml is None: |
74 | + return [] |
75 | + network_nodes = etree.fromstring(lshw_xml).xpath("//node[@id='network']") |
76 | + interfaces = [ |
77 | + extract_network_interface_data(xml_node) |
78 | + for xml_node in network_nodes |
79 | + ] |
80 | + return [ |
81 | + (interface, normalise_mac(mac)) |
82 | + for interface, mac in interfaces |
83 | + if interface is not None and mac is not None |
84 | + ] |
85 | + |
86 | + |
87 | +def generate_ethernet_link_entry(interface, mac): |
88 | + """Generate the `links` list entry for the given ethernet interface. |
89 | + |
90 | + :param interface: Network interface name, e.g. `eth0`. |
91 | + :param mac: MAC address, e.g. `00:11:22:33:44:55`. |
92 | + :return: A dict specifying the network interface, with keys |
93 | + `id`, `type`, and `ethernet_mac_address` (and possibly more). |
94 | + """ |
95 | + return { |
96 | + 'id': interface, |
97 | + 'type': 'ethernet', |
98 | + 'ethernet_mac_address': mac, |
99 | + } |
100 | + |
101 | + |
102 | +def generate_networking_config(node): |
103 | + """Generate a networking preseed for `node`. |
104 | + |
105 | + :param node: A `Node`. |
106 | + :return: A dict along the lines of the example in |
107 | + https://gist.github.com/jayofdoom/b035067523defec7fb53 -- just |
108 | + json-encode it to get a file in that format. |
109 | + """ |
110 | + interfaces = extract_network_interfaces(node) |
111 | + return { |
112 | + 'provider': "MAAS", |
113 | + 'network_info': { |
114 | + 'services': [ |
115 | + # List DNS servers here. |
116 | + ], |
117 | + 'networks': [ |
118 | + # Write network specs here. |
119 | + ], |
120 | + 'links': [ |
121 | + generate_ethernet_link_entry(interface, mac) |
122 | + for interface, mac in interfaces |
123 | + ], |
124 | + }, |
125 | + } |
126 | |
127 | === added file 'src/maasserver/tests/test_networking_preseed.py' |
128 | --- src/maasserver/tests/test_networking_preseed.py 1970-01-01 00:00:00 +0000 |
129 | +++ src/maasserver/tests/test_networking_preseed.py 2014-09-24 02:49:10 +0000 |
130 | @@ -0,0 +1,263 @@ |
131 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
132 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
133 | + |
134 | +"""Tests for networking preseed code.""" |
135 | + |
136 | +from __future__ import ( |
137 | + absolute_import, |
138 | + print_function, |
139 | + unicode_literals, |
140 | + ) |
141 | + |
142 | +str = None |
143 | + |
144 | +__metaclass__ = type |
145 | +__all__ = [ |
146 | + ] |
147 | + |
148 | +from maasserver import networking_preseed |
149 | +from maasserver.networking_preseed import ( |
150 | + extract_network_interfaces, |
151 | + generate_ethernet_link_entry, |
152 | + generate_networking_config, |
153 | + normalise_mac, |
154 | + ) |
155 | +from maasserver.testing.factory import factory |
156 | +from maasserver.testing.testcase import MAASServerTestCase |
157 | +from maastesting.matchers import MockCalledOnceWith |
158 | + |
159 | + |
160 | +def make_denormalised_mac(): |
161 | + return ' %s ' % factory.make_mac_address().upper() |
162 | + |
163 | + |
164 | +class TestExtractNetworkInterfaces(MAASServerTestCase): |
165 | + |
166 | + def test__returns_nothing_if_no_lshw_output_found(self): |
167 | + node = factory.make_Node() |
168 | + self.assertEqual([], extract_network_interfaces(node)) |
169 | + |
170 | + def test__returns_nothing_if_no_network_description_found_in_lshw(self): |
171 | + node = factory.make_Node() |
172 | + lshw_output = """ |
173 | + <list xmlns:lldp="lldp" xmlns:lshw="lshw"> |
174 | + <lshw:list> |
175 | + </lshw:list> |
176 | + </list> |
177 | + """ |
178 | + factory.make_NodeResult_for_commissioning( |
179 | + node=node, name='00-maas-01-lshw.out', script_result=0, |
180 | + data=lshw_output.encode('ascii')) |
181 | + self.assertEqual([], extract_network_interfaces(node)) |
182 | + |
183 | + def test__extracts_interface_data(self): |
184 | + node = factory.make_Node() |
185 | + interface = factory.make_name('eth') |
186 | + mac = factory.make_mac_address() |
187 | + lshw_output = """ |
188 | + <node id="network" claimed="true" class="network"> |
189 | + <logicalname>%(interface)s</logicalname> |
190 | + <serial>%(mac)s</serial> |
191 | + </node> |
192 | + """ % {'interface': interface, 'mac': mac} |
193 | + factory.make_NodeResult_for_commissioning( |
194 | + node=node, name='00-maas-01-lshw.out', script_result=0, |
195 | + data=lshw_output.encode('ascii')) |
196 | + self.assertEqual([(interface, mac)], extract_network_interfaces(node)) |
197 | + |
198 | + def test__finds_network_interface_on_motherboard(self): |
199 | + node = factory.make_Node() |
200 | + interface = factory.make_name('eth') |
201 | + mac = factory.make_mac_address() |
202 | + # Stripped-down version of real lshw output: |
203 | + lshw_output = """ |
204 | + <!-- generated by lshw-B.02.16 --> |
205 | + <list> |
206 | + <node id="mynode" claimed="true" class="system" handle="DMI:0002"> |
207 | + <node id="core" claimed="true" class="bus" handle="DMI:0003"> |
208 | + <description>Motherboard</description> |
209 | + <node id="pci" claimed="true" class="bridge" \ |
210 | + handle="PCIBUS:0000:00"> |
211 | + <description>Host bridge</description> |
212 | + <node id="network" claimed="true" class="network" \ |
213 | + handle="PCI:0000:00:19.0"> |
214 | + <description>Ethernet interface</description> |
215 | + <product>82566DM-2 Gigabit Network Connection</product> |
216 | + <vendor>Intel Corporation</vendor> |
217 | + <logicalname>%(interface)s</logicalname> |
218 | + <serial>%(mac)s</serial> |
219 | + <configuration> |
220 | + <setting id="ip" value="10.99.99.1" /> |
221 | + </configuration> |
222 | + </node> |
223 | + </node> |
224 | + </node> |
225 | + </node> |
226 | + </list> |
227 | + """ % {'interface': interface, 'mac': mac} |
228 | + factory.make_NodeResult_for_commissioning( |
229 | + node=node, name='00-maas-01-lshw.out', script_result=0, |
230 | + data=lshw_output.encode('ascii')) |
231 | + self.assertEqual([(interface, mac)], extract_network_interfaces(node)) |
232 | + |
233 | + def test__finds_network_interface_on_pci_bus(self): |
234 | + node = factory.make_Node() |
235 | + interface = factory.make_name('eth') |
236 | + mac = factory.make_mac_address() |
237 | + # Stripped-down version of real lshw output: |
238 | + lshw_output = """ |
239 | + <!-- generated by lshw-B.02.16 --> |
240 | + <list> |
241 | + <node id="mynode" claimed="true" class="system" handle="DMI:0002"> |
242 | + <node id="core" claimed="true" class="bus" handle="DMI:0003"> |
243 | + <description>Motherboard</description> |
244 | + <node id="pci" claimed="true" class="bridge" \ |
245 | + handle="PCIBUS:0000:00"> |
246 | + <description>Host bridge</description> |
247 | + <node id="pci:2" claimed="true" class="bridge" \ |
248 | + handle="PCIBUS:0000:07"> |
249 | + <description>PCI bridge</description> |
250 | + <node id="network" claimed="true" class="network" \ |
251 | + handle="PCI:0000:07:04.0"> |
252 | + <description>Ethernet interface</description> |
253 | + <logicalname>%(interface)s</logicalname> |
254 | + <serial>%(mac)s</serial> |
255 | + <configuration> |
256 | + <setting id="ip" value="192.168.1.114" /> |
257 | + </configuration> |
258 | + </node> |
259 | + </node> |
260 | + </node> |
261 | + </node> |
262 | + </node> |
263 | + </list> |
264 | + """ % {'interface': interface, 'mac': mac} |
265 | + factory.make_NodeResult_for_commissioning( |
266 | + node=node, name='00-maas-01-lshw.out', script_result=0, |
267 | + data=lshw_output.encode('ascii')) |
268 | + self.assertEqual([(interface, mac)], extract_network_interfaces(node)) |
269 | + |
270 | + def test__ignores_nodes_without_interface_name(self): |
271 | + node = factory.make_Node() |
272 | + mac = factory.make_mac_address() |
273 | + lshw_output = """ |
274 | + <node id="network" claimed="true" class="network"> |
275 | + <serial>%s</serial> |
276 | + </node> |
277 | + """ % mac |
278 | + factory.make_NodeResult_for_commissioning( |
279 | + node=node, name='00-maas-01-lshw.out', script_result=0, |
280 | + data=lshw_output.encode('ascii')) |
281 | + self.assertEqual([], extract_network_interfaces(node)) |
282 | + |
283 | + def test__ignores_nodes_without_mac(self): |
284 | + node = factory.make_Node() |
285 | + interface = factory.make_name('eth') |
286 | + lshw_output = """ |
287 | + <node id="network" claimed="true" class="network"> |
288 | + <logicalname>%s</logicalname> |
289 | + </node> |
290 | + """ % interface |
291 | + factory.make_NodeResult_for_commissioning( |
292 | + node=node, name='00-maas-01-lshw.out', script_result=0, |
293 | + data=lshw_output.encode('ascii')) |
294 | + self.assertEqual([], extract_network_interfaces(node)) |
295 | + |
296 | + def test__normalises_mac(self): |
297 | + node = factory.make_Node() |
298 | + interface = factory.make_name('eth') |
299 | + mac = make_denormalised_mac() |
300 | + self.assertNotEqual(normalise_mac(mac), mac) |
301 | + lshw_output = """ |
302 | + <node id="network" claimed="true" class="network"> |
303 | + <logicalname>%(interface)s</logicalname> |
304 | + <serial>%(mac)s</serial> |
305 | + </node> |
306 | + """ % {'interface': interface, 'mac': mac} |
307 | + factory.make_NodeResult_for_commissioning( |
308 | + node=node, name='00-maas-01-lshw.out', script_result=0, |
309 | + data=lshw_output.encode('ascii')) |
310 | + [entry] = extract_network_interfaces(node) |
311 | + _, extracted_mac = entry |
312 | + self.assertEqual(normalise_mac(mac), extracted_mac) |
313 | + |
314 | + |
315 | +class TestNormaliseMAC(MAASServerTestCase): |
316 | + |
317 | + def test__normalises_case(self): |
318 | + mac = factory.make_mac_address() |
319 | + self.assertEqual( |
320 | + normalise_mac(mac.lower()), |
321 | + normalise_mac(mac.upper())) |
322 | + |
323 | + def test__strips_whitespace(self): |
324 | + mac = factory.make_mac_address() |
325 | + self.assertEqual( |
326 | + normalise_mac(mac), |
327 | + normalise_mac(' %s ' % mac)) |
328 | + |
329 | + def test__is_idempotent(self): |
330 | + mac = factory.make_mac_address() |
331 | + self.assertEqual( |
332 | + normalise_mac(mac), |
333 | + normalise_mac(normalise_mac(mac))) |
334 | + |
335 | + |
336 | +class TestGenerateEthernetLinkEntry(MAASServerTestCase): |
337 | + |
338 | + def test__generates_dict(self): |
339 | + interface = factory.make_name('eth') |
340 | + mac = factory.make_mac_address() |
341 | + self.assertEqual( |
342 | + { |
343 | + 'id': interface, |
344 | + 'type': 'ethernet', |
345 | + 'ethernet_mac_address': mac, |
346 | + }, |
347 | + generate_ethernet_link_entry(interface, mac)) |
348 | + |
349 | + |
350 | +class TestGenerateNetworkingConfig(MAASServerTestCase): |
351 | + |
352 | + def patch_interfaces(self, interface_mac_pairs): |
353 | + patch = self.patch_autospec( |
354 | + networking_preseed, 'extract_network_interfaces') |
355 | + patch.return_value = interface_mac_pairs |
356 | + return patch |
357 | + |
358 | + def test__returns_config_dict(self): |
359 | + self.patch_interfaces([]) |
360 | + config = generate_networking_config(factory.make_Node()) |
361 | + self.assertIsInstance(config, dict) |
362 | + self.assertEqual("MAAS", config['provider']) |
363 | + |
364 | + def test__includes_links(self): |
365 | + node = factory.make_Node() |
366 | + interface = factory.make_name('eth') |
367 | + mac = factory.make_mac_address() |
368 | + patch = self.patch_interfaces([(interface, mac)]) |
369 | + |
370 | + config = generate_networking_config(node) |
371 | + |
372 | + self.assertThat(patch, MockCalledOnceWith(node)) |
373 | + self.assertEqual( |
374 | + [ |
375 | + { |
376 | + 'id': interface, |
377 | + 'type': 'ethernet', |
378 | + 'ethernet_mac_address': mac, |
379 | + }, |
380 | + ], |
381 | + config['network_info']['links']) |
382 | + |
383 | + def test__includes_networks(self): |
384 | + # This section is not yet implemented, so expect an empty list. |
385 | + self.patch_interfaces([]) |
386 | + config = generate_networking_config(factory.make_Node()) |
387 | + self.assertEqual([], config['network_info']['networks']) |
388 | + |
389 | + def test__includes_dns_servers(self): |
390 | + # This section is not yet implemented, so expect an empty list. |
391 | + self.patch_interfaces([]) |
392 | + config = generate_networking_config(factory.make_Node()) |
393 | + self.assertEqual([], config['network_info']['services']) |
Some minor tweaks needed for clarity, but looks good! Thanks :)