Merge lp:~jtv/maas/dhcp-config-helpers 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: 2809
Proposed branch: lp:~jtv/maas/dhcp-config-helpers
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~jtv/maas/dhcp-test-trivia
Diff against target: 294 lines (+193/-46)
2 files modified
src/maasserver/dhcp.py (+39/-17)
src/maasserver/tests/test_dhcp.py (+154/-29)
To merge this branch: bzr merge lp:~jtv/maas/dhcp-config-helpers
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+232035@code.launchpad.net

This proposal supersedes a proposal from 2014-08-25.

Commit message

Preparation for split IPv4/IPv6 DHCP configs: extract helper for building a subnet config, and add helper to separate IPv4 and IPv6 cluster interfaces.

Description of the change

I used the helper for building a subnet config to simplify the configure_dhcp and one of its tests. The helper for separating IPv4 and IPv6 cluster interfaces is to become useful in my next branch. The current configure_dhcp code isn't actually prepared to deal with DHCPv6, so my next step will be to make it ignore IPv6 cluster interfaces. After that I'll build separate support for those, using the new RPC call where the existing code uses Celery for the IPv4 equivalent.

We may want to postpone landing of those next changes until after our current "soft freeze." The branch you see here however should be quite safe.

Jeroen

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

Looks good. Couple of nits.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) :
Revision history for this message
MAAS Lander (maas-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/dhcp.py'
2--- src/maasserver/dhcp.py 2014-08-18 10:53:21 +0000
3+++ src/maasserver/dhcp.py 2014-08-26 03:16:24 +0000
4@@ -16,6 +16,8 @@
5 'configure_dhcp',
6 ]
7
8+from collections import defaultdict
9+
10 from django.conf import settings
11 from maasserver.enum import NODEGROUP_STATUS
12 from maasserver.models import Config
13@@ -40,6 +42,40 @@
14 return None
15
16
17+def split_ipv4_ipv6_interfaces(interfaces):
18+ """Divide `interfaces` into IPv4 ones and IPv6 ones.
19+
20+ :param interfaces: An iterable of cluster interfaces.
21+ :return: A tuple of two separate iterables: IPv4 cluster interfaces for
22+ `nodegroup`, and its IPv6 cluster interfaces.
23+ """
24+ split = defaultdict(list)
25+ for interface in interfaces:
26+ split[interface.network.version].append(interface)
27+ assert len(split.keys()) <= 2, (
28+ "Unexpected IP version(s): %s" % ', '.join(split.keys()))
29+ return split[4], split[6]
30+
31+
32+def make_subnet_config(interface, dns_servers, ntp_server):
33+ """Return DHCP subnet configuration dict for a cluster interface."""
34+ return {
35+ 'subnet': unicode(
36+ IPAddress(interface.ip_range_low) &
37+ IPAddress(interface.subnet_mask)),
38+ 'subnet_mask': interface.subnet_mask,
39+ 'subnet_cidr': unicode(interface.network.cidr),
40+ 'broadcast_ip': interface.broadcast_ip,
41+ 'interface': interface.interface,
42+ 'router_ip': unicode(interface.router_ip),
43+ 'dns_servers': dns_servers,
44+ 'ntp_server': ntp_server,
45+ 'domain_name': interface.nodegroup.name,
46+ 'ip_range_low': interface.ip_range_low,
47+ 'ip_range_high': interface.ip_range_high,
48+ }
49+
50+
51 def configure_dhcp(nodegroup):
52 """Write the DHCP configuration file and restart the DHCP server."""
53 # Let's get this out of the way first up shall we?
54@@ -70,24 +106,10 @@
55
56 dns_server = get_dns_server_address(nodegroup)
57 ntp_server = Config.objects.get_config("ntp_server")
58- dhcp_subnet_configs = []
59 dhcp_subnet_configs = [
60- dict(
61- subnet=unicode(
62- IPAddress(interface.ip_range_low) &
63- IPAddress(interface.subnet_mask)),
64- subnet_mask=interface.subnet_mask,
65- subnet_cidr=unicode(interface.network),
66- broadcast_ip=interface.broadcast_ip,
67- interface=interface.interface,
68- router_ip=interface.router_ip,
69- dns_servers=dns_server,
70- ntp_server=ntp_server,
71- domain_name=nodegroup.name,
72- ip_range_low=interface.ip_range_low,
73- ip_range_high=interface.ip_range_high,
74- )
75- for interface in interfaces]
76+ make_subnet_config(interface, dns_server, ntp_server)
77+ for interface in interfaces
78+ ]
79
80 reload_dhcp_server_subtask = restart_dhcp_server.subtask(
81 options={'queue': nodegroup.work_queue})
82
83=== modified file 'src/maasserver/tests/test_dhcp.py'
84--- src/maasserver/tests/test_dhcp.py 2014-08-25 07:09:30 +0000
85+++ src/maasserver/tests/test_dhcp.py 2014-08-26 03:16:24 +0000
86@@ -21,6 +21,8 @@
87 from maasserver.dhcp import (
88 configure_dhcp,
89 get_interfaces_managed_by,
90+ make_subnet_config,
91+ split_ipv4_ipv6_interfaces,
92 )
93 from maasserver.dns.zonegenerator import get_dns_server_address
94 from maasserver.enum import (
95@@ -41,11 +43,150 @@
96 from provisioningserver.utils.enum import map_enum
97 from testresources import FixtureResource
98 from testtools.matchers import (
99+ ContainsAll,
100 EndsWith,
101+ Equals,
102 HasLength,
103+ IsInstance,
104+ Not,
105 )
106
107
108+class TestSplitIPv4IPv6Interfaces(MAASServerTestCase):
109+ """Tests for `split_ipv4_ipv6_interfaces`."""
110+
111+ def make_ipv4_interface(self, nodegroup):
112+ return factory.make_node_group_interface(
113+ nodegroup, network=factory.getRandomNetwork())
114+
115+ def make_ipv6_interface(self, nodegroup):
116+ return factory.make_node_group_interface(
117+ nodegroup, network=factory.make_ipv6_network())
118+
119+ def test__separates_IPv4_from_IPv6_interfaces(self):
120+ nodegroup = factory.make_node_group()
121+ # Create 0-2 IPv4 cluster interfaces and 0-2 IPv6 cluster interfaces.
122+ ipv4_interfaces = [
123+ self.make_ipv4_interface(nodegroup)
124+ for _ in range(random.randint(0, 2))
125+ ]
126+ ipv6_interfaces = [
127+ self.make_ipv6_interface(nodegroup)
128+ for _ in range(random.randint(0, 2))
129+ ]
130+ interfaces = sorted(
131+ ipv4_interfaces + ipv6_interfaces,
132+ key=lambda *args: random.randint(0, 10))
133+
134+ ipv4_result, ipv6_result = split_ipv4_ipv6_interfaces(interfaces)
135+
136+ self.assertItemsEqual(ipv4_interfaces, ipv4_result)
137+ self.assertItemsEqual(ipv6_interfaces, ipv6_result)
138+
139+
140+class TestMakeSubnetConfig(MAASServerTestCase):
141+ """Tests for `make_subnet_config`."""
142+
143+ def test__includes_all_parameters(self):
144+ interface = factory.make_node_group_interface(
145+ factory.make_node_group())
146+ config = make_subnet_config(
147+ interface, factory.make_name('dns'), factory.make_name('ntp'))
148+ self.assertIsInstance(config, dict)
149+ self.assertThat(
150+ config.keys(),
151+ ContainsAll([
152+ 'subnet',
153+ 'subnet_mask',
154+ 'subnet_cidr',
155+ 'broadcast_ip',
156+ 'interface',
157+ 'router_ip',
158+ 'dns_servers',
159+ 'ntp_server',
160+ 'domain_name',
161+ 'ip_range_low',
162+ 'ip_range_high',
163+ ]))
164+
165+ def test__sets_dns_and_ntp_from_arguments(self):
166+ interface = factory.make_node_group_interface(
167+ factory.make_node_group())
168+ dns = '%s %s' % (
169+ factory.getRandomIPAddress(),
170+ factory.make_ipv6_address(),
171+ )
172+ ntp = factory.make_name('ntp')
173+ config = make_subnet_config(interface, dns_servers=dns, ntp_server=ntp)
174+ self.expectThat(config['dns_servers'], Equals(dns))
175+ self.expectThat(config['ntp_server'], Equals(ntp))
176+
177+ def test__sets_domain_name_from_cluster(self):
178+ nodegroup = factory.make_node_group()
179+ interface = factory.make_node_group_interface(nodegroup)
180+ config = make_subnet_config(
181+ interface, factory.make_name('dns'), factory.make_name('ntp'))
182+ self.expectThat(config['domain_name'], Equals(nodegroup.name))
183+
184+ def test__sets_other_items_from_interface(self):
185+ interface = factory.make_node_group_interface(
186+ factory.make_node_group())
187+ config = make_subnet_config(
188+ interface, factory.make_name('dns'), factory.make_name('ntp'))
189+ self.expectThat(config['broadcast_ip'], Equals(interface.broadcast_ip))
190+ self.expectThat(config['interface'], Equals(interface.interface))
191+ self.expectThat(config['router_ip'], Equals(interface.router_ip))
192+
193+ def test__passes_IP_addresses_as_strings(self):
194+ interface = factory.make_node_group_interface(
195+ factory.make_node_group())
196+ config = make_subnet_config(
197+ interface, factory.make_name('dns'), factory.make_name('ntp'))
198+ self.expectThat(config['subnet'], IsInstance(unicode))
199+ self.expectThat(config['subnet_mask'], IsInstance(unicode))
200+ self.expectThat(config['subnet_cidr'], IsInstance(unicode))
201+ self.expectThat(config['broadcast_ip'], IsInstance(unicode))
202+ self.expectThat(config['router_ip'], IsInstance(unicode))
203+ self.expectThat(config['ip_range_low'], IsInstance(unicode))
204+ self.expectThat(config['ip_range_high'], IsInstance(unicode))
205+
206+ def test__defines_IPv4_subnet(self):
207+ interface = factory.make_node_group_interface(
208+ factory.make_node_group(), network=IPNetwork('10.9.8.7/24'))
209+ config = make_subnet_config(
210+ interface, factory.make_name('dns'), factory.make_name('ntp'))
211+ self.expectThat(config['subnet'], Equals('10.9.8.0'))
212+ self.expectThat(config['subnet_mask'], Equals('255.255.255.0'))
213+ self.expectThat(config['subnet_cidr'], Equals('10.9.8.0/24'))
214+
215+ def test__defines_IPv6_subnet(self):
216+ interface = factory.make_node_group_interface(
217+ factory.make_node_group(),
218+ network=IPNetwork('fd38:c341:27da:c831::/64'))
219+ config = make_subnet_config(
220+ interface, factory.make_name('dns'), factory.make_name('ntp'))
221+ # Don't expect a specific literal value, like we do for IPv4; there
222+ # are different spellings.
223+ self.expectThat(
224+ IPAddress(config['subnet']),
225+ Equals(IPAddress('fd38:c341:27da:c831::')))
226+ # (Netmask is not used for the IPv6 config, so ignore it.)
227+ self.expectThat(
228+ IPNetwork(config['subnet_cidr']),
229+ Equals(IPNetwork('fd38:c341:27da:c831::/64')))
230+
231+ def test__passes_dynamic_range(self):
232+ interface = factory.make_node_group_interface(
233+ factory.make_node_group())
234+ config = make_subnet_config(
235+ interface, factory.make_name('dns'), factory.make_name('ntp'))
236+ self.expectThat(
237+ (config['ip_range_low'], config['ip_range_high']),
238+ Equals((interface.ip_range_low, interface.ip_range_high)))
239+ self.expectThat(
240+ config['ip_range_low'], Not(Equals(interface.static_ip_range_low)))
241+
242+
243 class TestGetInterfacesManagedBy(MAASServerTestCase):
244 """Tests for `get_interfaces_managed_by`."""
245
246@@ -123,35 +264,19 @@
247
248 configure_dhcp(nodegroup)
249
250- dhcp_subnets = []
251- for interface in nodegroup.get_managed_interfaces():
252- dhcp_params = [
253- 'interface',
254- 'subnet_mask',
255- 'broadcast_ip',
256- 'router_ip',
257- 'ip_range_low',
258- 'ip_range_high',
259- ]
260-
261- dhcp_subnet = {
262- param: getattr(interface, param)
263- for param in dhcp_params}
264- dhcp_subnet["dns_servers"] = get_dns_server_address()
265- dhcp_subnet["ntp_server"] = get_default_config()['ntp_server']
266- dhcp_subnet["domain_name"] = nodegroup.name
267- dhcp_subnet["subnet"] = unicode(
268- IPAddress(interface.ip_range_low) &
269- IPAddress(interface.subnet_mask))
270- dhcp_subnet["subnet_cidr"] = unicode(interface.network)
271- dhcp_subnets.append(dhcp_subnet)
272-
273- expected_params = {}
274- expected_params["omapi_key"] = nodegroup.dhcp_key
275- expected_params["dhcp_interfaces"] = ' '.join([
276- interface.interface
277- for interface in nodegroup.get_managed_interfaces()])
278- expected_params["dhcp_subnets"] = dhcp_subnets
279+ expected_params = {
280+ 'omapi_key': nodegroup.dhcp_key,
281+ 'dhcp_interfaces': ' '.join([
282+ interface.interface
283+ for interface in nodegroup.get_managed_interfaces()
284+ ]),
285+ 'dhcp_subnets': [
286+ make_subnet_config(
287+ interface, get_dns_server_address(),
288+ get_default_config()['ntp_server'])
289+ for interface in nodegroup.get_managed_interfaces()
290+ ],
291+ }
292
293 args, kwargs = mocked_task.apply_async.call_args
294 result_params = kwargs['kwargs']