Merge lp:~jtv/maas/generate-node-networking-config-2 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: 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
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

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

Great work, Jeroen. Thank you for persevering with this!

Just a few nitpicks; nothing major.

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

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://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Hit http://security.ubuntu.com trusty-security Release
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
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
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
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/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
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 python-openssl python-paramiko python-pexpect python-pip python-pocket-lint python-psycopg2 python-pyinotify python-seamicroclient python-simplejson python-simplestreams python-...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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'])