Merge lp:~rvb/maas/dhcp-multiple-intf into lp:~maas-committers/maas/trunk

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 1859
Proposed branch: lp:~rvb/maas/dhcp-multiple-intf
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 402 lines (+132/-95)
7 files modified
etc/maas/templates/dhcp/dhcpd.conf.template (+12/-9)
src/maasserver/dhcp.py (+24/-18)
src/maasserver/tests/test_dhcp.py (+53/-23)
src/provisioningserver/dhcp/config.py (+3/-19)
src/provisioningserver/dhcp/tests/test_config.py (+24/-20)
src/provisioningserver/dhcp/writer.py (+9/-3)
src/provisioningserver/tests/test_tasks.py (+7/-3)
To merge this branch: bzr merge lp:~rvb/maas/dhcp-multiple-intf
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+203325@code.launchpad.net

Commit message

Generate a multiple-network DHCP config.

Description of the change

This branch changes the DHCP-config writing code so that it can write multiple-network DHCP configs. I've tested that there is no regression by building a package from this branch and testing it in the lab.

Now, we're still in the process of testing if it's worth adding an "interface <intf>" statement inside each subnet definition. If it works, we will fix this as a follow-up branch.

To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Thanks! In a way I think the code got simpler here — the network information is now more encapsulated.

A small note: in rc/provisioningserver/dhcp/config.py (starting around line 45) you now have...

    except KeyError as error:
        raise DHCPConfigError(*error.args)
    except NameError as error:
        raise DHCPConfigError(*error.args)

You can write that as simply:

    except (KeyError, NameError) as error:
        raise DHCPConfigError(*error.args)

Python's authors have thought of these things. :-)

The big loop in src/maasserver/dhcp.py (starting around line 60) looks like it now wants to be a list comprehension, with its body extracted into a separate function. But no biggie.

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

Thanks for the review!

> A small note: in rc/provisioningserver/dhcp/config.py (starting around line
> 45) you now have...
>
> except KeyError as error:
> raise DHCPConfigError(*error.args)
> except NameError as error:
> raise DHCPConfigError(*error.args)
>
> You can write that as simply:
>
> except (KeyError, NameError) as error:
> raise DHCPConfigError(*error.args)
>

Right, fixed (I think I was a bit tired when I wrote this code ;))

> The big loop in src/maasserver/dhcp.py (starting around line 60) looks like it
> now wants to be a list comprehension, with its body extracted into a separate
> function. But no biggie.

Actually, a list comprehension will be enough to make the code a tad more simple. Fixed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'etc/maas/templates/dhcp/dhcpd.conf.template'
2--- etc/maas/templates/dhcp/dhcpd.conf.template 2013-11-11 06:31:34 +0000
3+++ etc/maas/templates/dhcp/dhcpd.conf.template 2014-01-29 08:28:06 +0000
4@@ -5,24 +5,27 @@
5 # will be present whenever MAAS rewrites the DHCP configuration. Edit and save
6 # the nodegroup's configuration in MAAS to trigger an update.
7
8-subnet {{subnet}} netmask {{subnet_mask}} {
9+{{for dhcp_subnet in dhcp_subnets}}
10+subnet {{dhcp_subnet['subnet']}} netmask {{dhcp_subnet['subnet_mask']}} {
11 filename "{{bootloader}}";
12 ignore-client-uids true;
13- option subnet-mask {{subnet_mask}};
14- option broadcast-address {{broadcast_ip}};
15- option domain-name-servers {{dns_servers}};
16- option domain-name "{{domain_name}}";
17- option routers {{router_ip}};
18- {{if ntp_server}}
19- option ntp-servers {{ntp_server}};
20+ option subnet-mask {{dhcp_subnet['subnet_mask']}};
21+ option broadcast-address {{dhcp_subnet['broadcast_ip']}};
22+ option domain-name-servers {{dhcp_subnet['dns_servers']}};
23+ option domain-name "{{dhcp_subnet['domain_name']}}";
24+ option routers {{dhcp_subnet['router_ip']}};
25+ {{if dhcp_subnet.get('ntp_server')}}
26+ option ntp-servers {{dhcp_subnet['ntp_server']}};
27 {{endif}}
28- range dynamic-bootp {{ip_range_low}} {{ip_range_high}};
29+ range dynamic-bootp {{dhcp_subnet['ip_range_low']}} {{dhcp_subnet['ip_range_high']}};
30 class "PXE" {
31 match if substring (option vendor-class-identifier, 0, 3) = "PXE";
32 default-lease-time 30;
33 max-lease-time 30;
34 }
35 }
36+{{endfor}}
37+
38 omapi-port 7911;
39 key omapi_key {
40 algorithm HMAC-MD5;
41
42=== modified file 'src/maasserver/dhcp.py'
43--- src/maasserver/dhcp.py 2014-01-17 09:57:37 +0000
44+++ src/maasserver/dhcp.py 2014-01-29 08:28:06 +0000
45@@ -17,9 +17,7 @@
46 ]
47
48 from django.conf import settings
49-from maasserver.enum import (
50- NODEGROUP_STATUS,
51- )
52+from maasserver.enum import NODEGROUP_STATUS
53 from maasserver.models import Config
54 from netaddr import IPAddress
55 from provisioningserver.tasks import (
56@@ -55,25 +53,33 @@
57 # server.
58 nodegroup.ensure_dhcp_key()
59
60- for interface in interfaces:
61- subnet = unicode(
62- IPAddress(interface.ip_range_low) &
63- IPAddress(interface.subnet_mask))
64- reload_dhcp_server_subtask = restart_dhcp_server.subtask(
65- options={'queue': nodegroup.work_queue})
66- task_kwargs = dict(
67- subnet=subnet,
68- omapi_key=nodegroup.dhcp_key,
69+ dns_server = get_dns_server_address(nodegroup)
70+ ntp_server = Config.objects.get_config("ntp_server")
71+ dhcp_subnet_configs = []
72+ dhcp_subnet_configs = [
73+ dict(
74+ subnet=unicode(
75+ IPAddress(interface.ip_range_low) &
76+ IPAddress(interface.subnet_mask)),
77 subnet_mask=interface.subnet_mask,
78- dhcp_interfaces=interface.interface,
79 broadcast_ip=interface.broadcast_ip,
80 router_ip=interface.router_ip,
81- dns_servers=get_dns_server_address(nodegroup),
82- ntp_server=Config.objects.get_config("ntp_server"),
83+ dns_servers=dns_server,
84+ ntp_server=ntp_server,
85 domain_name=nodegroup.name,
86 ip_range_low=interface.ip_range_low,
87 ip_range_high=interface.ip_range_high,
88- callback=reload_dhcp_server_subtask,
89 )
90- write_dhcp_config.apply_async(
91- queue=nodegroup.work_queue, kwargs=task_kwargs)
92+ for interface in interfaces]
93+
94+ reload_dhcp_server_subtask = restart_dhcp_server.subtask(
95+ options={'queue': nodegroup.work_queue})
96+ task_kwargs = dict(
97+ dhcp_subnets=dhcp_subnet_configs,
98+ omapi_key=nodegroup.dhcp_key,
99+ dhcp_interfaces=' '.join(
100+ [interface.interface for interface in interfaces]),
101+ callback=reload_dhcp_server_subtask,
102+ )
103+ write_dhcp_config.apply_async(
104+ queue=nodegroup.work_queue, kwargs=task_kwargs)
105
106=== modified file 'src/maasserver/tests/test_dhcp.py'
107--- src/maasserver/tests/test_dhcp.py 2014-01-27 12:48:10 +0000
108+++ src/maasserver/tests/test_dhcp.py 2014-01-29 08:28:06 +0000
109@@ -24,14 +24,20 @@
110 get_interfaces_managed_by,
111 )
112 from maasserver.dns import get_dns_server_address
113-from maasserver.enum import NODEGROUP_STATUS
114+from maasserver.enum import (
115+ NODEGROUP_STATUS,
116+ NODEGROUPINTERFACE_MANAGEMENT,
117+ )
118 from maasserver.models import Config
119 from maasserver.models.config import get_default_config
120 from maasserver.testing.factory import factory
121 from maasserver.testing.testcase import MAASServerTestCase
122 from maasserver.utils import map_enum
123 from maastesting.celery import CeleryFixture
124-from netaddr import IPNetwork
125+from netaddr import (
126+ IPAddress,
127+ IPNetwork,
128+ )
129 from provisioningserver import tasks
130 from testresources import FixtureResource
131 from testtools.matchers import EndsWith
132@@ -72,38 +78,62 @@
133 {status: [] for status in unaccepted_statuses},
134 managed_interfaces)
135
136+ def enable_multiple_managed_interfaces(self):
137+ # XXX: rvb 2012-09-18 bug=1052339: Only one "managed" interface
138+ # is supported per NodeGroup.
139+ # This can be removed once NodeGroupInterface.clean_management
140+ # goes away.
141+ from maasserver.models.nodegroupinterface import NodeGroupInterface
142+ self.patch(NodeGroupInterface, 'clean_management')
143+
144 def test_configure_dhcp_writes_dhcp_config(self):
145+ self.enable_multiple_managed_interfaces()
146 mocked_task = self.patch(dhcp, 'write_dhcp_config')
147 self.patch(
148 settings, 'DEFAULT_MAAS_URL',
149 'http://%s/' % factory.getRandomIPAddress())
150+ self.patch(settings, "DHCP_CONNECT", True)
151 nodegroup = factory.make_node_group(
152 status=NODEGROUP_STATUS.ACCEPTED,
153 dhcp_key=factory.getRandomString(),
154 interface=factory.make_name('eth'),
155 network=IPNetwork("192.168.102.0/22"))
156+ # Create a second DHCP-managed interface.
157+ factory.make_node_group_interface(
158+ nodegroup=nodegroup,
159+ interface=factory.make_name('eth'),
160+ network=IPNetwork("10.1.1/24"),
161+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
162+ )
163
164- self.patch(settings, "DHCP_CONNECT", True)
165 configure_dhcp(nodegroup)
166- dhcp_params = [
167- 'subnet_mask',
168- 'broadcast_ip',
169- 'router_ip',
170- 'ip_range_low',
171- 'ip_range_high',
172- ]
173-
174- [interface] = nodegroup.get_managed_interfaces()
175- expected_params = {
176- param: getattr(interface, param)
177- for param in dhcp_params}
178-
179+ dhcp_subnets = []
180+ for interface in nodegroup.get_managed_interfaces():
181+ dhcp_params = [
182+ 'subnet_mask',
183+ 'broadcast_ip',
184+ 'router_ip',
185+ 'ip_range_low',
186+ 'ip_range_high',
187+ ]
188+
189+ dhcp_subnet = {
190+ param: getattr(interface, param)
191+ for param in dhcp_params}
192+ dhcp_subnet["dns_servers"] = get_dns_server_address()
193+ dhcp_subnet["ntp_server"] = get_default_config()['ntp_server']
194+ dhcp_subnet["domain_name"] = nodegroup.name
195+ dhcp_subnet["subnet"] = unicode(
196+ IPAddress(interface.ip_range_low) &
197+ IPAddress(interface.subnet_mask))
198+ dhcp_subnets.append(dhcp_subnet)
199+
200+ expected_params = {}
201 expected_params["omapi_key"] = nodegroup.dhcp_key
202- expected_params["dns_servers"] = get_dns_server_address()
203- expected_params["ntp_server"] = get_default_config()['ntp_server']
204- expected_params["domain_name"] = nodegroup.name
205- expected_params["subnet"] = '192.168.100.0'
206- expected_params["dhcp_interfaces"] = interface.interface
207+ expected_params["dhcp_interfaces"] = ' '.join([
208+ interface.interface
209+ for interface in nodegroup.get_managed_interfaces()])
210+ expected_params["dhcp_subnets"] = dhcp_subnets
211
212 args, kwargs = mocked_task.apply_async.call_args
213 result_params = kwargs['kwargs']
214@@ -127,7 +157,7 @@
215 configure_dhcp(nodegroup)
216 kwargs = mocked_task.apply_async.call_args[1]['kwargs']
217
218- self.assertEqual(ip, kwargs['dns_servers'])
219+ self.assertEqual(ip, kwargs['dhcp_subnets'][0]['dns_servers'])
220
221 def test_configure_dhcp_restart_dhcp_server(self):
222 self.patch(tasks, "sudo_write_file")
223@@ -200,7 +230,7 @@
224 (1, new_router_ip),
225 (
226 dhcp.write_dhcp_config.apply_async.call_count,
227- kwargs['kwargs']['router_ip'],
228+ kwargs['kwargs']['dhcp_subnets'][0]['router_ip'],
229 ))
230
231 def test_dhcp_config_gets_written_when_ntp_server_changes(self):
232
233=== modified file 'src/provisioningserver/dhcp/config.py'
234--- src/provisioningserver/dhcp/config.py 2013-11-11 06:31:34 +0000
235+++ src/provisioningserver/dhcp/config.py 2014-01-29 08:28:06 +0000
236@@ -33,23 +33,7 @@
237
238
239 def get_config(**params):
240- """Return a DHCP config file based on the supplied parameters.
241-
242- :param subnet: The base subnet declaration. e.g. 192.168.1.0
243- :param subnet_mask: The mask for the above subnet, e.g. 255.255.255.0
244- :param broadcast_address: The broadcast IP address for the subnet,
245- e.g. 192.168.1.255
246- :param dns_servers: One or more IP addresses of the DNS server for the
247- subnet
248- :param ntp_server: IP address of the NTP server for the nodes
249- :param domain_name: name that will be appended to the client's hostname to
250- form a fully-qualified domain-name
251- :param gateway: The router/gateway IP address for the subnet.
252- :param low_range: The first IP address in the range of IP addresses to
253- allocate
254- :param high_range: The last IP address in the range of IP addresses to
255- allocate
256- """
257+ """Return a DHCP config file based on the supplied parameters."""
258 template_file = locate_config(TEMPLATES_DIR, 'dhcpd.conf.template')
259 params['bootloader'] = compose_bootloader_path()
260 params['platform_codename'] = linux_distribution()[2]
261@@ -57,6 +41,6 @@
262 try:
263 template = tempita.Template.from_filename(
264 template_file, encoding="UTF-8")
265- return template.substitute(params)
266- except NameError as error:
267+ return template.substitute(**params)
268+ except (KeyError, NameError) as error:
269 raise DHCPConfigError(*error.args)
270
271=== modified file 'src/provisioningserver/dhcp/tests/test_config.py'
272--- src/provisioningserver/dhcp/tests/test_config.py 2014-01-21 08:28:39 +0000
273+++ src/provisioningserver/dhcp/tests/test_config.py 2014-01-29 08:28:06 +0000
274@@ -35,14 +35,16 @@
275 # substitutions, but none that aren't also in the real template.
276 sample_template = dedent("""\
277 {{omapi_key}}
278- {{subnet}}
279- {{subnet_mask}}
280- {{broadcast_ip}}
281- {{dns_servers}}
282- {{domain_name}}
283- {{router_ip}}
284- {{ip_range_low}}
285- {{ip_range_high}}
286+ {{for dhcp_subnet in dhcp_subnets}}
287+ {{dhcp_subnet['subnet']}}
288+ {{dhcp_subnet['subnet_mask']}}
289+ {{dhcp_subnet['broadcast_ip']}}
290+ {{dhcp_subnet['dns_servers']}}
291+ {{dhcp_subnet['domain_name']}}
292+ {{dhcp_subnet['router_ip']}}
293+ {{dhcp_subnet['ip_range_low']}}
294+ {{dhcp_subnet['ip_range_high']}}
295+ {{endfor}}
296 """)
297
298
299@@ -53,15 +55,17 @@
300 """
301 return dict(
302 omapi_key="random",
303- subnet="10.0.0.0",
304- subnet_mask="255.0.0.0",
305- broadcast_ip="10.255.255.255",
306- dns_servers="10.1.0.1 10.1.0.2",
307- ntp_server="8.8.8.8",
308- domain_name="example.com",
309- router_ip="10.0.0.2",
310- ip_range_low="10.0.0.3",
311- ip_range_high="10.0.0.254",
312+ dhcp_subnets=[dict(
313+ subnet="10.0.0.0",
314+ subnet_mask="255.0.0.0",
315+ broadcast_ip="10.255.255.255",
316+ dns_servers="10.1.0.1 10.1.0.2",
317+ ntp_server="8.8.8.8",
318+ domain_name="example.com",
319+ router_ip="10.0.0.2",
320+ ip_range_low="10.0.0.3",
321+ ip_range_high="10.0.0.254",
322+ )]
323 )
324
325
326@@ -98,14 +102,14 @@
327 def test_get_config_with_too_few_parameters(self):
328 template = self.patch_template()
329 params = make_sample_params()
330- del params['subnet']
331+ del params['dhcp_subnets'][0]['subnet']
332
333 e = self.assertRaises(
334 config.DHCPConfigError, config.get_config, **params)
335
336 self.assertThat(
337 unicode(e), MatchesRegex(
338- "name 'subnet' is not defined at line \d+ column \d+ "
339+ "subnet at line \d+ column \d+ "
340 "in file %s" % template.name))
341
342 def test_config_refers_to_bootloader(self):
343@@ -115,7 +119,7 @@
344
345 def test_renders_without_ntp_servers_set(self):
346 params = make_sample_params()
347- del params['ntp_server']
348+ del params['dhcp_subnets'][0]['ntp_server']
349 template = self.patch_template()
350 rendered = template.substitute(params)
351 self.assertEqual(rendered, config.get_config(**params))
352
353=== modified file 'src/provisioningserver/dhcp/writer.py'
354--- src/provisioningserver/dhcp/writer.py 2013-11-11 06:24:19 +0000
355+++ src/provisioningserver/dhcp/writer.py 2014-01-29 08:28:06 +0000
356@@ -72,9 +72,15 @@
357 def run(args):
358 """Generate a DHCP server configuration, and write it to stdout."""
359 params = vars(args)
360- output = config.get_config(**params).encode("ascii")
361- if args.outfile is None:
362+ omapi_key = params.pop('omapi_key')
363+ outfile = params.pop('outfile')
364+ kwargs = {
365+ 'dhcp_subnets': [params],
366+ 'omapi_key': omapi_key,
367+ }
368+ output = config.get_config(**kwargs).encode("ascii")
369+ if outfile is None:
370 sys.stdout.write(output)
371 else:
372- with open(args.outfile, "wb") as out:
373+ with open(outfile, "wb") as out:
374 out.write(output)
375
376=== modified file 'src/provisioningserver/tests/test_tasks.py'
377--- src/provisioningserver/tests/test_tasks.py 2014-01-22 03:54:03 +0000
378+++ src/provisioningserver/tests/test_tasks.py 2014-01-29 08:28:06 +0000
379@@ -177,7 +177,6 @@
380 """Fake up a dict of dhcp configuration parameters."""
381 param_names = [
382 'interface',
383- 'omapi_key',
384 'subnet',
385 'subnet_mask',
386 'broadcast_ip',
387@@ -186,8 +185,13 @@
388 'router_ip',
389 'ip_range_low',
390 'ip_range_high',
391- ]
392- return {param: factory.getRandomString() for param in param_names}
393+ ]
394+ return {
395+ 'dhcp_subnets': [
396+ {param: factory.getRandomString() for param in param_names}
397+ ],
398+ 'omapi_key': factory.getRandomString(),
399+ }
400
401 def test_upload_dhcp_leases(self):
402 self.patch(