Merge lp:~jtv/maas/configure-dhcpv6 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: 2845
Proposed branch: lp:~jtv/maas/configure-dhcpv6
Merge into: lp:~maas-committers/maas/trunk
Prerequisite: lp:~jtv/maas/bits-of-rpc-docstring
Diff against target: 316 lines (+112/-28)
3 files modified
src/maasserver/dhcp.py (+28/-2)
src/maasserver/rpc/testing/fixtures.py (+7/-1)
src/maasserver/tests/test_dhcp.py (+77/-25)
To merge this branch: bzr merge lp:~jtv/maas/configure-dhcpv6
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+232192@code.launchpad.net

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

Commit message

Implement RPC call to configure DHCPv6 server. This includes Gavin's patch to the live cluster-service fixture, and his help getting the tests working.

Description of the change

Discussed with Julian. It bugs us that the front-end may be waiting for cluster controllers, so filed this as bug 1361590 for further discussion.

Jeroen

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Tip top.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

Holding this branch back from landing because it could break things, which we're not supposed to do at the moment.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Tuesday 26 August 2014 12:58:22 you wrote:
> > + client = getClientFor(nodegroup.uuid)
>
> Might be worth noting in the docstring that this can raise
> NoConnectionsAvailable.

Actually I think it should be catching them here and using the event log to
log errors. However as I feared would happen, the event log is node-based
which doesn't suit everything we need to log.

Maybe we need a new type of event log that logs system wide errors that need
to be in the admin's face, and add some UI for it.

At the very least, catch the error here and report it, otherwise pages like
the cluster interface edit page would 500 when clusters are down.

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

At last got this through Q/A, after discovering that the problems were caused by bug 1363105. The IPv6 side may not be complete yet, but it doesn't appear to break IPv4, so I'm landing. A node can ping another node by name, which shows that DHCP tells it how to reach the DNS server.

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-26 07:58:55 +0000
3+++ src/maasserver/dhcp.py 2014-08-29 02:35:01 +0000
4@@ -21,12 +21,15 @@
5 from django.conf import settings
6 from maasserver.enum import NODEGROUP_STATUS
7 from maasserver.models import Config
8+from maasserver.rpc import getClientFor
9 from netaddr import IPAddress
10+from provisioningserver.rpc.cluster import ConfigureDHCPv6
11 from provisioningserver.tasks import (
12 restart_dhcp_server,
13 stop_dhcp_server,
14 write_dhcp_config,
15 )
16+from provisioningserver.utils.twisted import synchronous
17
18
19 def split_ipv4_ipv6_interfaces(interfaces):
20@@ -93,9 +96,32 @@
21 queue=nodegroup.work_queue, kwargs=task_kwargs)
22
23
24+@synchronous
25 def configure_dhcpv6(nodegroup, interfaces, dns_server, ntp_server):
26- """Write DHCPv6 configuration and restart the DHCPv6 server."""
27- # TODO: Implement.
28+ """Write DHCPv6 configuration and restart the DHCPv6 server.
29+
30+ Delegates the work to the cluster controller, and waits for it
31+ to complete.
32+
33+ :raise NoConnectionsAvailable: if the region controller could not get
34+ an RPC connection to the cluster controller.
35+ :raise CannotConfigureDHCP: if configuration could not be written, or
36+ restart of the DHCP server fails.
37+ """
38+ # XXX jtv 2014-08-26 bug=1361590: UI/API requests to update cluster
39+ # interfaces will block on this. We may need an asynchronous error
40+ # backchannel.
41+ subnets = [
42+ make_subnet_config(interface, dns_server, ntp_server)
43+ for interface in interfaces
44+ ]
45+ client = getClientFor(nodegroup.uuid)
46+ # XXX jtv 2014-08-26 bug=1361673: If this fails remotely, the error
47+ # needs to be reported gracefully to the caller.
48+ call = client(
49+ ConfigureDHCPv6, omapi_key=nodegroup.dhcp_key, subnet_configs=subnets)
50+ # Keep the timeout short: the UI may be waiting for completion.
51+ call.wait(5)
52
53
54 def configure_dhcp(nodegroup):
55
56=== modified file 'src/maasserver/rpc/testing/fixtures.py'
57--- src/maasserver/rpc/testing/fixtures.py 2014-08-26 09:23:08 +0000
58+++ src/maasserver/rpc/testing/fixtures.py 2014-08-29 02:35:01 +0000
59@@ -28,6 +28,7 @@
60 from maasserver import eventloop
61 from maasserver.enum import NODEGROUP_STATUS
62 from maasserver.models.nodegroup import NodeGroup
63+from maasserver.rpc import getClientFor
64 from maasserver.rpc.regionservice import RegionServer
65 from maasserver.testing.eventloop import (
66 RegionEventLoopFixture,
67@@ -317,4 +318,9 @@
68 ident_response = {"ident": nodegroup.uuid.decode("ascii")}
69 protocol.Identify.side_effect = (
70 lambda protocol: defer.succeed(ident_response.copy()))
71- return self.addCluster(protocol).wait(10)
72+ self.addCluster(protocol).wait(10)
73+ # The connection is now established, but there is a brief handshake
74+ # that takes place immediately upon connection. We wait for that to
75+ # finish before returning.
76+ getClientFor(nodegroup.uuid, timeout=5)
77+ return protocol
78
79=== modified file 'src/maasserver/tests/test_dhcp.py'
80--- src/maasserver/tests/test_dhcp.py 2014-08-26 08:13:11 +0000
81+++ src/maasserver/tests/test_dhcp.py 2014-08-29 02:35:01 +0000
82@@ -21,6 +21,7 @@
83 from maasserver.dhcp import (
84 configure_dhcp,
85 configure_dhcpv4,
86+ configure_dhcpv6,
87 make_subnet_config,
88 split_ipv4_ipv6_interfaces,
89 )
90@@ -30,6 +31,11 @@
91 NODEGROUPINTERFACE_MANAGEMENT,
92 )
93 from maasserver.models import Config
94+from maasserver.rpc.testing.fixtures import MockLiveRegionToClusterRPCFixture
95+from maasserver.testing.eventloop import (
96+ RegionEventLoopFixture,
97+ RunningEventLoopFixture,
98+ )
99 from maasserver.testing.factory import factory
100 from maasserver.testing.testcase import MAASServerTestCase
101 from maastesting.celery import CeleryFixture
102@@ -43,6 +49,7 @@
103 IPNetwork,
104 )
105 from provisioningserver import tasks
106+from provisioningserver.rpc.cluster import ConfigureDHCPv6
107 from testresources import FixtureResource
108 from testtools.matchers import (
109 ContainsAll,
110@@ -297,6 +304,37 @@
111 self.assertEqual(nodegroup.work_queue, kwargs['options']['queue'])
112
113
114+class TestConfigureDHCPv6(MAASServerTestCase):
115+ """Tests for `configure_dhcpv6`."""
116+
117+ def prepare_rpc(self, nodegroup):
118+ """Set up test case for speaking RPC to `nodegroup`.
119+
120+ :param nodegroup: A cluster. It will "run" a mock RPC service.
121+ :return: Protocol.
122+ """
123+ self.useFixture(RegionEventLoopFixture('rpc'))
124+ self.useFixture(RunningEventLoopFixture())
125+ fixture = self.useFixture(MockLiveRegionToClusterRPCFixture())
126+ return fixture.makeCluster(nodegroup, ConfigureDHCPv6)
127+
128+ def test__configures_dhcpv6(self):
129+ nodegroup = factory.make_node_group(
130+ status=NODEGROUP_STATUS.ACCEPTED,
131+ management=NODEGROUPINTERFACE_MANAGEMENT.DHCP,
132+ dhcp_key=factory.make_name('key'),
133+ network=factory.make_ipv6_network())
134+ protocol = self.prepare_rpc(nodegroup)
135+
136+ configure_dhcpv6(
137+ nodegroup, [], factory.make_name('dns'), factory.make_name('ntp'))
138+
139+ self.assertThat(
140+ protocol.ConfigureDHCPv6,
141+ MockCalledOnceWith(
142+ ANY, omapi_key=nodegroup.dhcp_key, subnet_configs=[]))
143+
144+
145 class TestConfigureDHCP(MAASServerTestCase):
146 """Tests for `configure_dhcp`."""
147
148@@ -455,103 +493,116 @@
149 )
150
151 def test_dhcp_config_gets_written_when_nodegroup_becomes_active(self):
152+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
153+ self.patch(dhcp, 'configure_dhcpv6')
154 nodegroup = factory.make_node_group(
155 status=NODEGROUP_STATUS.PENDING,
156 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
157 self.patch(settings, "DHCP_CONNECT", True)
158- self.patch(dhcp, 'write_dhcp_config')
159 nodegroup.accept()
160- self.assertEqual(1, dhcp.write_dhcp_config.apply_async.call_count)
161+ self.assertThat(
162+ configure_dhcpv4, MockCalledOnceWith(nodegroup, ANY, ANY, ANY))
163
164 def test_dhcp_config_gets_written_when_nodegroup_name_changes(self):
165+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
166+ self.patch(dhcp, 'configure_dhcpv6')
167 nodegroup = factory.make_node_group(
168 status=NODEGROUP_STATUS.ACCEPTED,
169 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
170 self.patch(settings, "DHCP_CONNECT", True)
171- self.patch(dhcp, 'write_dhcp_config')
172- new_name = factory.make_name('dns name'),
173
174- nodegroup.name = new_name
175+ nodegroup.name = factory.make_name('domain')
176 nodegroup.save()
177
178- self.assertEqual(1, dhcp.write_dhcp_config.apply_async.call_count)
179+ self.assertThat(
180+ configure_dhcpv4, MockCalledOnceWith(nodegroup, ANY, ANY, ANY))
181
182 def test_dhcp_config_gets_written_when_interface_IP_changes(self):
183+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
184+ self.patch(dhcp, 'configure_dhcpv6')
185 nodegroup = factory.make_node_group(
186 status=NODEGROUP_STATUS.ACCEPTED,
187 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
188 [interface] = nodegroup.nodegroupinterface_set.all()
189 self.patch(settings, "DHCP_CONNECT", True)
190- self.patch(dhcp, 'write_dhcp_config')
191
192 interface.ip = factory.pick_ip_in_network(
193 interface.network, but_not=[interface.ip])
194 interface.save()
195
196- self.assertEqual(1, dhcp.write_dhcp_config.apply_async.call_count)
197+ self.assertThat(
198+ configure_dhcpv4, MockCalledOnceWith(nodegroup, ANY, ANY, ANY))
199
200 def test_dhcp_config_gets_written_when_interface_management_changes(self):
201+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
202+ self.patch(dhcp, 'configure_dhcpv6')
203 nodegroup = factory.make_node_group(
204 status=NODEGROUP_STATUS.ACCEPTED,
205 management=NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED)
206 [interface] = nodegroup.nodegroupinterface_set.all()
207 self.patch(settings, "DHCP_CONNECT", True)
208- self.patch(dhcp, 'write_dhcp_config')
209
210 interface.management = NODEGROUPINTERFACE_MANAGEMENT.DHCP
211 interface.save()
212
213- self.assertEqual(1, dhcp.write_dhcp_config.apply_async.call_count)
214+ self.assertThat(
215+ configure_dhcpv4, MockCalledOnceWith(nodegroup, ANY, ANY, ANY))
216
217 def test_dhcp_config_gets_written_when_interface_name_changes(self):
218+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
219+ self.patch(dhcp, 'configure_dhcpv6')
220 nodegroup = factory.make_node_group(
221 status=NODEGROUP_STATUS.ACCEPTED,
222 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
223 [interface] = nodegroup.get_managed_interfaces()
224 self.patch(settings, "DHCP_CONNECT", True)
225- self.patch(dhcp, 'write_dhcp_config')
226
227 interface.interface = factory.make_name('itf')
228 interface.save()
229
230- self.assertEqual(1, dhcp.write_dhcp_config.apply_async.call_count)
231+ self.assertThat(
232+ configure_dhcpv4, MockCalledOnceWith(nodegroup, ANY, ANY, ANY))
233
234 def test_dhcp_config_gets_written_when_netmask_changes(self):
235+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
236+ self.patch(dhcp, 'configure_dhcpv6')
237 network = factory.getRandomNetwork(slash='255.255.255.0')
238 nodegroup = factory.make_node_group(
239 status=NODEGROUP_STATUS.ACCEPTED, network=network,
240 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
241 [interface] = nodegroup.get_managed_interfaces()
242 self.patch(settings, "DHCP_CONNECT", True)
243- self.patch(dhcp, 'write_dhcp_config')
244
245 interface.subnet_mask = '255.255.0.0'
246 interface.save()
247
248- self.assertEqual(1, dhcp.write_dhcp_config.apply_async.call_count)
249+ self.assertThat(
250+ configure_dhcpv4, MockCalledOnceWith(nodegroup, ANY, ANY, ANY))
251
252 def test_dhcp_config_gets_written_when_interface_router_ip_changes(self):
253+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
254+ self.patch(dhcp, 'configure_dhcpv6')
255 nodegroup = factory.make_node_group(
256 status=NODEGROUP_STATUS.ACCEPTED,
257 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
258 [interface] = nodegroup.get_managed_interfaces()
259 self.patch(settings, "DHCP_CONNECT", True)
260- self.patch(dhcp, 'write_dhcp_config')
261- new_router_ip = factory.pick_ip_in_network(
262+
263+ interface.router_ip = factory.pick_ip_in_network(
264 interface.network, but_not=[interface.router_ip])
265-
266- interface.router_ip = new_router_ip
267 interface.save()
268
269- self.assertEqual(1, dhcp.write_dhcp_config.apply_async.call_count)
270+ self.assertThat(
271+ configure_dhcpv4, MockCalledOnceWith(nodegroup, ANY, ANY, ANY))
272
273 def test_dhcp_config_gets_written_when_ip_range_changes(self):
274+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
275+ self.patch(dhcp, 'configure_dhcpv6')
276 nodegroup = factory.make_node_group(
277 status=NODEGROUP_STATUS.ACCEPTED,
278 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
279 [interface] = nodegroup.get_managed_interfaces()
280 self.patch(settings, "DHCP_CONNECT", True)
281- self.patch(dhcp, 'write_dhcp_config')
282
283 interface.ip_range_low = unicode(
284 IPAddress(interface.ip_range_low) + 1)
285@@ -559,25 +610,26 @@
286 IPAddress(interface.ip_range_high) - 1)
287 interface.save()
288
289- self.assertEqual(1, dhcp.write_dhcp_config.apply_async.call_count)
290+ self.assertThat(
291+ configure_dhcpv4, MockCalledOnceWith(nodegroup, ANY, ANY, ANY))
292
293 def test_dhcp_config_is_not_written_when_foreign_dhcp_changes(self):
294+ configure_dhcpv4 = self.patch(dhcp, 'configure_dhcpv4')
295 nodegroup = factory.make_node_group(
296 status=NODEGROUP_STATUS.ACCEPTED,
297 management=NODEGROUPINTERFACE_MANAGEMENT.DHCP)
298 [interface] = nodegroup.get_managed_interfaces()
299- self.patch(dhcp, 'write_dhcp_config')
300 self.patch(settings, "DHCP_CONNECT", True)
301
302- interface.foreign_dhcp = factory.pick_ip_in_network(
303- interface.network)
304+ interface.foreign_dhcp = factory.pick_ip_in_network(interface.network)
305 interface.save()
306
307- self.assertEqual([], dhcp.write_dhcp_config.apply_async.mock_calls)
308+ self.assertThat(configure_dhcpv4, MockNotCalled())
309
310 def test_dhcp_config_gets_written_when_ntp_server_changes(self):
311 # When the "ntp_server" Config item is changed, check that all
312 # nodegroups get their DHCP config re-written.
313+ self.patch(dhcp, 'configure_dhcpv6')
314 num_active_nodegroups = random.randint(1, 10)
315 num_inactive_nodegroups = random.randint(1, 10)
316 for x in range(num_active_nodegroups):