Merge lp:~jtv/maas/generate-node-networking-config-1 into lp:~maas-committers/maas/trunk

Proposed by Jeroen T. Vermeulen
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
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

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Some minor tweaks needed for clarity, but looks good! Thanks :)

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

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://security.ubuntu.com trusty-security InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Get:2 http://security.ubuntu.com trusty-security Release [59.7 kB]
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [59.7 kB]
Get:5 http://security.ubuntu.com trusty-security/main Sources [44.9 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Get:6 http://security.ubuntu.com trusty-security/universe Sources [10.8 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [144 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [48.5 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [120 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [85.2 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [322 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [205 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Fetched 1,101 kB in 0s (1,753 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi py...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'src/maasserver/networking_preseed.py'
--- src/maasserver/networking_preseed.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/networking_preseed.py 2014-09-24 02:49:10 +0000
@@ -0,0 +1,121 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Network configuration preseed code.
5
6This will eventually generate installer networking configuration like:
7
8 https://gist.github.com/jayofdoom/b035067523defec7fb53
9
10The installer running on a node will use this to set up the node's networking
11according to MAAS's specifications.
12"""
13
14from __future__ import (
15 absolute_import,
16 print_function,
17 unicode_literals,
18 )
19
20str = None
21
22__metaclass__ = type
23__all__ = [
24 'generate_networking_config',
25 ]
26
27from lxml import etree
28from maasserver.models.nodeprobeddetails import get_probed_details
29
30
31def extract_network_interface_data(element):
32 """Extract network interface name and MAC address from XML element.
33
34 :return: A tuple of network interface name and MAC address as found in
35 the XML. If either is not found, it will be `None`.
36 """
37 interfaces = element.xpath("logicalname")
38 macs = element.xpath("serial")
39 if len(interfaces) == 0 or len(macs) == 0:
40 # Not enough data.
41 return None, None
42 assert len(interfaces) == 1
43 assert len(macs) == 1
44 return interfaces[0].text, macs[0].text
45
46
47def normalise_mac(mac):
48 """Return a MAC's normalised representation.
49
50 This doesn't actually parse all the different formats for writing MAC
51 addresses, but it does eliminate case differences. In practice, any MAC
52 address this code is likely to will equal another version of itself after
53 normalisation, even if they were originally different spellings.
54 """
55 return mac.strip().lower()
56
57
58def extract_network_interfaces(node):
59 """Extract network interfaces from node's `lshw` output.
60
61 :param node: A `Node`.
62 :return: A list of tuples describing the network interfaces.
63 Each tuple consists of an interface name and a MAC address.
64 """
65 node_details = get_probed_details([node.system_id])
66 if node.system_id not in node_details:
67 return []
68 lshw_xml = node_details[node.system_id].get('lshw')
69 if lshw_xml is None:
70 return []
71 network_nodes = etree.fromstring(lshw_xml).xpath("//node[@id='network']")
72 interfaces = [
73 extract_network_interface_data(xml_node)
74 for xml_node in network_nodes
75 ]
76 return [
77 (interface, normalise_mac(mac))
78 for interface, mac in interfaces
79 if interface is not None and mac is not None
80 ]
81
82
83def generate_ethernet_link_entry(interface, mac):
84 """Generate the `links` list entry for the given ethernet interface.
85
86 :param interface: Network interface name, e.g. `eth0`.
87 :param mac: MAC address, e.g. `00:11:22:33:44:55`.
88 :return: A dict specifying the network interface, with keys
89 `id`, `type`, and `ethernet_mac_address` (and possibly more).
90 """
91 return {
92 'id': interface,
93 'type': 'ethernet',
94 'ethernet_mac_address': mac,
95 }
96
97
98def generate_networking_config(node):
99 """Generate a networking preseed for `node`.
100
101 :param node: A `Node`.
102 :return: A dict along the lines of the example in
103 https://gist.github.com/jayofdoom/b035067523defec7fb53 -- just
104 json-encode it to get a file in that format.
105 """
106 interfaces = extract_network_interfaces(node)
107 return {
108 'provider': "MAAS",
109 'network_info': {
110 'services': [
111 # List DNS servers here.
112 ],
113 'networks': [
114 # Write network specs here.
115 ],
116 'links': [
117 generate_ethernet_link_entry(interface, mac)
118 for interface, mac in interfaces
119 ],
120 },
121 }
0122
=== added file 'src/maasserver/tests/test_networking_preseed.py'
--- src/maasserver/tests/test_networking_preseed.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_networking_preseed.py 2014-09-24 02:49:10 +0000
@@ -0,0 +1,263 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for networking preseed code."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 ]
17
18from maasserver import networking_preseed
19from maasserver.networking_preseed import (
20 extract_network_interfaces,
21 generate_ethernet_link_entry,
22 generate_networking_config,
23 normalise_mac,
24 )
25from maasserver.testing.factory import factory
26from maasserver.testing.testcase import MAASServerTestCase
27from maastesting.matchers import MockCalledOnceWith
28
29
30def make_denormalised_mac():
31 return ' %s ' % factory.make_mac_address().upper()
32
33
34class TestExtractNetworkInterfaces(MAASServerTestCase):
35
36 def test__returns_nothing_if_no_lshw_output_found(self):
37 node = factory.make_Node()
38 self.assertEqual([], extract_network_interfaces(node))
39
40 def test__returns_nothing_if_no_network_description_found_in_lshw(self):
41 node = factory.make_Node()
42 lshw_output = """
43 <list xmlns:lldp="lldp" xmlns:lshw="lshw">
44 <lshw:list>
45 </lshw:list>
46 </list>
47 """
48 factory.make_NodeResult_for_commissioning(
49 node=node, name='00-maas-01-lshw.out', script_result=0,
50 data=lshw_output.encode('ascii'))
51 self.assertEqual([], extract_network_interfaces(node))
52
53 def test__extracts_interface_data(self):
54 node = factory.make_Node()
55 interface = factory.make_name('eth')
56 mac = factory.make_mac_address()
57 lshw_output = """
58 <node id="network" claimed="true" class="network">
59 <logicalname>%(interface)s</logicalname>
60 <serial>%(mac)s</serial>
61 </node>
62 """ % {'interface': interface, 'mac': mac}
63 factory.make_NodeResult_for_commissioning(
64 node=node, name='00-maas-01-lshw.out', script_result=0,
65 data=lshw_output.encode('ascii'))
66 self.assertEqual([(interface, mac)], extract_network_interfaces(node))
67
68 def test__finds_network_interface_on_motherboard(self):
69 node = factory.make_Node()
70 interface = factory.make_name('eth')
71 mac = factory.make_mac_address()
72 # Stripped-down version of real lshw output:
73 lshw_output = """
74 <!-- generated by lshw-B.02.16 -->
75 <list>
76 <node id="mynode" claimed="true" class="system" handle="DMI:0002">
77 <node id="core" claimed="true" class="bus" handle="DMI:0003">
78 <description>Motherboard</description>
79 <node id="pci" claimed="true" class="bridge" \
80 handle="PCIBUS:0000:00">
81 <description>Host bridge</description>
82 <node id="network" claimed="true" class="network" \
83 handle="PCI:0000:00:19.0">
84 <description>Ethernet interface</description>
85 <product>82566DM-2 Gigabit Network Connection</product>
86 <vendor>Intel Corporation</vendor>
87 <logicalname>%(interface)s</logicalname>
88 <serial>%(mac)s</serial>
89 <configuration>
90 <setting id="ip" value="10.99.99.1" />
91 </configuration>
92 </node>
93 </node>
94 </node>
95 </node>
96 </list>
97 """ % {'interface': interface, 'mac': mac}
98 factory.make_NodeResult_for_commissioning(
99 node=node, name='00-maas-01-lshw.out', script_result=0,
100 data=lshw_output.encode('ascii'))
101 self.assertEqual([(interface, mac)], extract_network_interfaces(node))
102
103 def test__finds_network_interface_on_pci_bus(self):
104 node = factory.make_Node()
105 interface = factory.make_name('eth')
106 mac = factory.make_mac_address()
107 # Stripped-down version of real lshw output:
108 lshw_output = """
109 <!-- generated by lshw-B.02.16 -->
110 <list>
111 <node id="mynode" claimed="true" class="system" handle="DMI:0002">
112 <node id="core" claimed="true" class="bus" handle="DMI:0003">
113 <description>Motherboard</description>
114 <node id="pci" claimed="true" class="bridge" \
115 handle="PCIBUS:0000:00">
116 <description>Host bridge</description>
117 <node id="pci:2" claimed="true" class="bridge" \
118 handle="PCIBUS:0000:07">
119 <description>PCI bridge</description>
120 <node id="network" claimed="true" class="network" \
121 handle="PCI:0000:07:04.0">
122 <description>Ethernet interface</description>
123 <logicalname>%(interface)s</logicalname>
124 <serial>%(mac)s</serial>
125 <configuration>
126 <setting id="ip" value="192.168.1.114" />
127 </configuration>
128 </node>
129 </node>
130 </node>
131 </node>
132 </node>
133 </list>
134 """ % {'interface': interface, 'mac': mac}
135 factory.make_NodeResult_for_commissioning(
136 node=node, name='00-maas-01-lshw.out', script_result=0,
137 data=lshw_output.encode('ascii'))
138 self.assertEqual([(interface, mac)], extract_network_interfaces(node))
139
140 def test__ignores_nodes_without_interface_name(self):
141 node = factory.make_Node()
142 mac = factory.make_mac_address()
143 lshw_output = """
144 <node id="network" claimed="true" class="network">
145 <serial>%s</serial>
146 </node>
147 """ % mac
148 factory.make_NodeResult_for_commissioning(
149 node=node, name='00-maas-01-lshw.out', script_result=0,
150 data=lshw_output.encode('ascii'))
151 self.assertEqual([], extract_network_interfaces(node))
152
153 def test__ignores_nodes_without_mac(self):
154 node = factory.make_Node()
155 interface = factory.make_name('eth')
156 lshw_output = """
157 <node id="network" claimed="true" class="network">
158 <logicalname>%s</logicalname>
159 </node>
160 """ % interface
161 factory.make_NodeResult_for_commissioning(
162 node=node, name='00-maas-01-lshw.out', script_result=0,
163 data=lshw_output.encode('ascii'))
164 self.assertEqual([], extract_network_interfaces(node))
165
166 def test__normalises_mac(self):
167 node = factory.make_Node()
168 interface = factory.make_name('eth')
169 mac = make_denormalised_mac()
170 self.assertNotEqual(normalise_mac(mac), mac)
171 lshw_output = """
172 <node id="network" claimed="true" class="network">
173 <logicalname>%(interface)s</logicalname>
174 <serial>%(mac)s</serial>
175 </node>
176 """ % {'interface': interface, 'mac': mac}
177 factory.make_NodeResult_for_commissioning(
178 node=node, name='00-maas-01-lshw.out', script_result=0,
179 data=lshw_output.encode('ascii'))
180 [entry] = extract_network_interfaces(node)
181 _, extracted_mac = entry
182 self.assertEqual(normalise_mac(mac), extracted_mac)
183
184
185class TestNormaliseMAC(MAASServerTestCase):
186
187 def test__normalises_case(self):
188 mac = factory.make_mac_address()
189 self.assertEqual(
190 normalise_mac(mac.lower()),
191 normalise_mac(mac.upper()))
192
193 def test__strips_whitespace(self):
194 mac = factory.make_mac_address()
195 self.assertEqual(
196 normalise_mac(mac),
197 normalise_mac(' %s ' % mac))
198
199 def test__is_idempotent(self):
200 mac = factory.make_mac_address()
201 self.assertEqual(
202 normalise_mac(mac),
203 normalise_mac(normalise_mac(mac)))
204
205
206class TestGenerateEthernetLinkEntry(MAASServerTestCase):
207
208 def test__generates_dict(self):
209 interface = factory.make_name('eth')
210 mac = factory.make_mac_address()
211 self.assertEqual(
212 {
213 'id': interface,
214 'type': 'ethernet',
215 'ethernet_mac_address': mac,
216 },
217 generate_ethernet_link_entry(interface, mac))
218
219
220class TestGenerateNetworkingConfig(MAASServerTestCase):
221
222 def patch_interfaces(self, interface_mac_pairs):
223 patch = self.patch_autospec(
224 networking_preseed, 'extract_network_interfaces')
225 patch.return_value = interface_mac_pairs
226 return patch
227
228 def test__returns_config_dict(self):
229 self.patch_interfaces([])
230 config = generate_networking_config(factory.make_Node())
231 self.assertIsInstance(config, dict)
232 self.assertEqual("MAAS", config['provider'])
233
234 def test__includes_links(self):
235 node = factory.make_Node()
236 interface = factory.make_name('eth')
237 mac = factory.make_mac_address()
238 patch = self.patch_interfaces([(interface, mac)])
239
240 config = generate_networking_config(node)
241
242 self.assertThat(patch, MockCalledOnceWith(node))
243 self.assertEqual(
244 [
245 {
246 'id': interface,
247 'type': 'ethernet',
248 'ethernet_mac_address': mac,
249 },
250 ],
251 config['network_info']['links'])
252
253 def test__includes_networks(self):
254 # This section is not yet implemented, so expect an empty list.
255 self.patch_interfaces([])
256 config = generate_networking_config(factory.make_Node())
257 self.assertEqual([], config['network_info']['networks'])
258
259 def test__includes_dns_servers(self):
260 # This section is not yet implemented, so expect an empty list.
261 self.patch_interfaces([])
262 config = generate_networking_config(factory.make_Node())
263 self.assertEqual([], config['network_info']['services'])