Merge lp:~allenap/maas/tftp-rebind-on-netif-change into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 1945
Proposed branch: lp:~allenap/maas/tftp-rebind-on-netif-change
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 384 lines (+203/-58)
5 files modified
services/pserv/run (+1/-0)
src/provisioningserver/plugin.py (+4/-16)
src/provisioningserver/tests/test_plugin.py (+14/-41)
src/provisioningserver/tests/test_tftp.py (+107/-0)
src/provisioningserver/tftp.py (+77/-1)
To merge this branch: bzr merge lp:~allenap/maas/tftp-rebind-on-netif-change
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Julian Edwards (community) Approve
Review via email: mp+206060@code.launchpad.net

Commit message

Rebind TFTP services to new interfaces when pserv received SIGHUP.

Previously the TFTP services would only be bound to those interfaces configured at start time.

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

Are we using threads anywhere in pserv (other than spawning one after the HUP)? Signals don't play nice with threads but it may be moot if Twisted is DTRT.

Anyway, lovely change.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> Are we using threads anywhere in pserv (other than spawning one after the
> HUP)? Signals don't play nice with threads but it may be moot if Twisted is
> DTRT.

Nope, don't think we're using threads. However, I've been cautious;
reactor.callFromThread will DTRT whether the handler runs in the reactor
thread or outside.

>
> Anyway, lovely change.

Thanks for the review!

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

Looks good although I'm not familiar enough with twisted to understand 100% of it. At always when making important changes to a core piece of the infrastructure, I recommend building a package from this branch and testing it before landing that whole lot.

[0]

188 +class TestTFTPService(MAASTestCase):
189 +
190 + def test_tftp_service(self):

This test is a bit painful to digest: it checks a ton of stuff. Could you think of a way to split it into more focused tests?

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> Looks good although I'm not familiar enough with twisted to understand 100% of
> it.  At always when making important changes to a core piece of the
> infrastructure, I recommend building a package from this branch and testing it
> before landing that whole lot.

This is not something I'm in the habit of doing, but I ought to.

>
> [0]
>
> 188     +class TestTFTPService(MAASTestCase):
> 189     +
> 190     +    def test_tftp_service(self):
>
> This test is a bit painful to digest: it checks a ton of stuff.  Could you
> think of a way to split it into more focused tests?

Breaking it up would mean a lot of repetition, getting it to the right
state. The liberal use of matchers will, I hope, make any failures
easier to understand.

Thanks for the re-review!

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

On Thursday 13 Feb 2014 14:50:55 you wrote:
> > Looks good although I'm not familiar enough with twisted to understand
> > 100% of it. At always when making important changes to a core piece of
> > the infrastructure, I recommend building a package from this branch and
> > testing it before landing that whole lot.
>
> This is not something I'm in the habit of doing, but I ought to.

FTR it's a total piece of cake to do this.

Get the packaging branch and at its top level dir type:

bzr builddeb

And that's it :) The packages will be placed in ../build-area/

You may need to edit a couple of things:
1. Set the revno in the top debian/changelog version if it's not new enough
2. For speed's sake, edit debian/rules and find the reference to lp:maas in
the get-orig-source rule and change it to use your local trunk branch.

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

+ # TimerService ensures that a call is made to it's target

its*

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'services/pserv/run'
--- services/pserv/run 2013-08-30 09:20:25 +0000
+++ services/pserv/run 2014-02-13 12:29:56 +0000
@@ -21,6 +21,7 @@
21# Obtain the development setting for CLUSTER_UUID.21# Obtain the development setting for CLUSTER_UUID.
22. etc/demo_maas_cluster.conf22. etc/demo_maas_cluster.conf
23export CLUSTER_UUID23export CLUSTER_UUID
24export MAAS_URL
2425
25exec $(command -v authbind && echo --deep) \26exec $(command -v authbind && echo --deep) \
26 "${script}" --nodaemon --pidfile="" maas-pserv --config-file "${config}"27 "${script}" --nodaemon --pidfile="" maas-pserv --config-file "${config}"
2728
=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py 2014-02-10 17:38:08 +0000
+++ src/provisioningserver/plugin.py 2014-02-13 12:29:56 +0000
@@ -21,10 +21,7 @@
21 LogService,21 LogService,
22 OOPSService,22 OOPSService,
23 )23 )
24from provisioningserver.tftp import TFTPBackend24from provisioningserver.tftp import TFTPService
25from provisioningserver.utils import get_all_interface_addresses
26from tftp.protocol import TFTP
27from twisted.application import internet
28from twisted.application.internet import (25from twisted.application.internet import (
29 TCPClient,26 TCPClient,
30 TCPServer,27 TCPServer,
@@ -154,18 +151,9 @@
154151
155 def _makeTFTPService(self, tftp_config):152 def _makeTFTPService(self, tftp_config):
156 """Create the dynamic TFTP service."""153 """Create the dynamic TFTP service."""
157 backend = TFTPBackend(tftp_config["root"], tftp_config["generator"])154 tftp_service = TFTPService(**tftp_config)
158 # Create a UDP server individually for each discovered network155 tftp_service.setName("tftp")
159 # interface, so that we can detect the interface via which we have156 return tftp_service
160 # received a datagram.
161 tftp_services = MultiService()
162 tftp_services.setName("tftp")
163 for address in get_all_interface_addresses():
164 tftp_service = internet.UDPServer(
165 tftp_config["port"], TFTP(backend), interface=address)
166 tftp_service.setName(address)
167 tftp_service.setServiceParent(tftp_services)
168 return tftp_services
169157
170 def _makeRPCService(self, rpc_config):158 def _makeRPCService(self, rpc_config):
171 rpc_service = ClusterClientService(reactor)159 rpc_service = ClusterClientService(reactor)
172160
=== modified file 'src/provisioningserver/tests/test_plugin.py'
--- src/provisioningserver/tests/test_plugin.py 2014-01-31 16:14:52 +0000
+++ src/provisioningserver/tests/test_plugin.py 2014-02-13 12:29:56 +0000
@@ -19,29 +19,29 @@
1919
20from maastesting.factory import factory20from maastesting.factory import factory
21from maastesting.testcase import MAASTestCase21from maastesting.testcase import MAASTestCase
22from provisioningserver import plugin
23from provisioningserver.plugin import (22from provisioningserver.plugin import (
24 Options,23 Options,
25 ProvisioningRealm,24 ProvisioningRealm,
26 ProvisioningServiceMaker,25 ProvisioningServiceMaker,
27 SingleUsernamePasswordChecker,26 SingleUsernamePasswordChecker,
28 )27 )
29from provisioningserver.tftp import TFTPBackend28from provisioningserver.tftp import (
29 TFTPBackend,
30 TFTPService,
31 )
30from testtools.deferredruntest import (32from testtools.deferredruntest import (
31 assert_fails_with,33 assert_fails_with,
32 AsynchronousDeferredRunTest,34 AsynchronousDeferredRunTest,
33 )35 )
34from testtools.matchers import (36from testtools.matchers import (
35 AfterPreprocessing,37 AfterPreprocessing,
36 AllMatch,
37 Equals,38 Equals,
38 IsInstance,39 IsInstance,
39 MatchesAll,40 MatchesAll,
40 MatchesException,41 MatchesException,
42 MatchesStructure,
41 Raises,43 Raises,
42 )44 )
43from tftp.protocol import TFTP
44from twisted.application.internet import UDPServer
45from twisted.application.service import MultiService45from twisted.application.service import MultiService
46from twisted.cred.credentials import UsernamePassword46from twisted.cred.credentials import UsernamePassword
47from twisted.cred.error import UnauthorizedLogin47from twisted.cred.error import UnauthorizedLogin
@@ -128,13 +128,6 @@
128128
129 def test_tftp_service(self):129 def test_tftp_service(self):
130 # A TFTP service is configured and added to the top-level service.130 # A TFTP service is configured and added to the top-level service.
131 interfaces = [
132 factory.getRandomIPAddress(),
133 factory.getRandomIPAddress(),
134 ]
135 self.patch(
136 plugin, "get_all_interface_addresses",
137 lambda: interfaces)
138 config = {131 config = {
139 "tftp": {132 "tftp": {
140 "generator": "http://candlemass/solitude",133 "generator": "http://candlemass/solitude",
@@ -146,14 +139,9 @@
146 options["config-file"] = self.write_config(config)139 options["config-file"] = self.write_config(config)
147 service_maker = ProvisioningServiceMaker("Harry", "Hill")140 service_maker = ProvisioningServiceMaker("Harry", "Hill")
148 service = service_maker.makeService(options)141 service = service_maker.makeService(options)
149 tftp_services = service.getServiceNamed("tftp")142 tftp_service = service.getServiceNamed("tftp")
150 # The "tftp" service is a multi-service containing UDP servers for143 self.assertIsInstance(tftp_service, TFTPService)
151 # each interface defined by get_all_interface_addresses().144
152 self.assertIsInstance(tftp_services, MultiService)
153 services = [
154 tftp_services.getServiceNamed(interface)
155 for interface in interfaces
156 ]
157 expected_backend = MatchesAll(145 expected_backend = MatchesAll(
158 IsInstance(TFTPBackend),146 IsInstance(TFTPBackend),
159 AfterPreprocessing(147 AfterPreprocessing(
@@ -162,27 +150,12 @@
162 AfterPreprocessing(150 AfterPreprocessing(
163 lambda backend: backend.generator_url.geturl(),151 lambda backend: backend.generator_url.geturl(),
164 Equals(config["tftp"]["generator"])))152 Equals(config["tftp"]["generator"])))
165 expected_protocol = MatchesAll(153
166 IsInstance(TFTP),154 self.assertThat(
167 AfterPreprocessing(155 tftp_service, MatchesStructure(
168 lambda protocol: protocol.backend,156 backend=expected_backend,
169 expected_backend))157 port=Equals(config["tftp"]["port"]),
170 expected_service = MatchesAll(158 ))
171 IsInstance(UDPServer),
172 AfterPreprocessing(
173 lambda service: len(service.args),
174 Equals(2)),
175 AfterPreprocessing(
176 lambda service: service.args[0], # port
177 Equals(config["tftp"]["port"])),
178 AfterPreprocessing(
179 lambda service: service.args[1], # protocol
180 expected_protocol))
181 self.assertThat(services, AllMatch(expected_service))
182 # Only the interface used for each service differs.
183 self.assertEqual(
184 [svc.kwargs for svc in services],
185 [{"interface": interface} for interface in interfaces])
186159
187160
188class TestSingleUsernamePasswordChecker(MAASTestCase):161class TestSingleUsernamePasswordChecker(MAASTestCase):
189162
=== modified file 'src/provisioningserver/tests/test_tftp.py'
--- src/provisioningserver/tests/test_tftp.py 2013-10-15 11:35:10 +0000
+++ src/provisioningserver/tests/test_tftp.py 2014-02-13 12:29:56 +0000
@@ -31,9 +31,22 @@
31from provisioningserver.tftp import (31from provisioningserver.tftp import (
32 BytesReader,32 BytesReader,
33 TFTPBackend,33 TFTPBackend,
34 TFTPService,
34 )35 )
35from testtools.deferredruntest import AsynchronousDeferredRunTest36from testtools.deferredruntest import AsynchronousDeferredRunTest
37from testtools.matchers import (
38 AfterPreprocessing,
39 AllMatch,
40 Equals,
41 IsInstance,
42 MatchesAll,
43 MatchesStructure,
44 )
36from tftp.backend import IReader45from tftp.backend import IReader
46from tftp.protocol import TFTP
47from twisted.application import internet
48from twisted.application.service import MultiService
49from twisted.internet import reactor
37from twisted.internet.defer import (50from twisted.internet.defer import (
38 inlineCallbacks,51 inlineCallbacks,
39 succeed,52 succeed,
@@ -288,3 +301,97 @@
288 self.assertEqual(fake_render_result.encode("utf-8"), output)301 self.assertEqual(fake_render_result.encode("utf-8"), output)
289 backend.render_pxe_config.assert_called_once_with(302 backend.render_pxe_config.assert_called_once_with(
290 kernel_params=fake_kernel_params, **fake_params)303 kernel_params=fake_kernel_params, **fake_params)
304
305
306class TestTFTPService(MAASTestCase):
307
308 def test_tftp_service(self):
309 # A TFTP service is configured and added to the top-level service.
310 interfaces = [
311 factory.getRandomIPAddress(),
312 factory.getRandomIPAddress(),
313 ]
314 self.patch(
315 tftp_module, "get_all_interface_addresses",
316 lambda: interfaces)
317 example_root = self.make_dir()
318 example_generator = "http://example.com/generator"
319 example_port = factory.getRandomPort()
320 tftp_service = TFTPService(
321 root=example_root, generator=example_generator,
322 port=example_port)
323 tftp_service.updateServers()
324 # The "tftp" service is a multi-service containing UDP servers for
325 # each interface defined by get_all_interface_addresses().
326 self.assertIsInstance(tftp_service, MultiService)
327 # There's also a TimerService that updates the servers every 45s.
328 self.assertThat(
329 tftp_service.refresher, MatchesStructure.byEquality(
330 step=45, parent=tftp_service, name="refresher",
331 call=(tftp_service.updateServers, (), {}),
332 ))
333 expected_backend = MatchesAll(
334 IsInstance(TFTPBackend),
335 AfterPreprocessing(
336 lambda backend: backend.base.path,
337 Equals(example_root)),
338 AfterPreprocessing(
339 lambda backend: backend.generator_url.geturl(),
340 Equals(example_generator)))
341 expected_protocol = MatchesAll(
342 IsInstance(TFTP),
343 AfterPreprocessing(
344 lambda protocol: protocol.backend,
345 expected_backend))
346 expected_server = MatchesAll(
347 IsInstance(internet.UDPServer),
348 AfterPreprocessing(
349 lambda service: len(service.args),
350 Equals(2)),
351 AfterPreprocessing(
352 lambda service: service.args[0], # port
353 Equals(example_port)),
354 AfterPreprocessing(
355 lambda service: service.args[1], # protocol
356 expected_protocol))
357 self.assertThat(
358 tftp_service.getServers(),
359 AllMatch(expected_server))
360 # Only the interface used for each service differs.
361 self.assertItemsEqual(
362 [svc.kwargs for svc in tftp_service.getServers()],
363 [{"interface": interface} for interface in interfaces])
364
365 def test_tftp_service_rebinds_on_HUP(self):
366 # Initial set of interfaces to bind to.
367 interfaces = {"1.1.1.1", "2.2.2.2"}
368 self.patch(
369 tftp_module, "get_all_interface_addresses",
370 lambda: interfaces)
371
372 tftp_service = TFTPService(
373 root=self.make_dir(), generator="http://mighty/wind",
374 port=factory.getRandomPort())
375 tftp_service.updateServers()
376
377 # The child services of tftp_services are named after the
378 # interface they bind to.
379 self.assertEqual(interfaces, {
380 server.name for server in tftp_service.getServers()
381 })
382
383 # Update the set of interfaces to bind to.
384 interfaces.add("3.3.3.3")
385 interfaces.remove("1.1.1.1")
386
387 # Ask the TFTP service to update its set of servers.
388 tftp_service.updateServers()
389
390 # We're in the reactor thread but we want to move the reactor
391 # forwards, hence we need to get all explicit about it.
392 reactor.runUntilCurrent()
393
394 # The interfaces now bound match the updated interfaces set.
395 self.assertEqual(interfaces, {
396 server.name for server in tftp_service.getServers()
397 })
291398
=== modified file 'src/provisioningserver/tftp.py'
--- src/provisioningserver/tftp.py 2013-10-15 11:35:10 +0000
+++ src/provisioningserver/tftp.py 2014-02-13 12:29:56 +0000
@@ -14,6 +14,7 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = [15__all__ = [
16 "TFTPBackend",16 "TFTPBackend",
17 "TFTPService",
17 ]18 ]
1819
19import httplib20import httplib
@@ -31,12 +32,18 @@
31from provisioningserver.enum import ARP_HTYPE32from provisioningserver.enum import ARP_HTYPE
32from provisioningserver.kernel_opts import KernelParameters33from provisioningserver.kernel_opts import KernelParameters
33from provisioningserver.pxe.config import render_pxe_config34from provisioningserver.pxe.config import render_pxe_config
34from provisioningserver.utils import deferred35from provisioningserver.utils import (
36 deferred,
37 get_all_interface_addresses,
38 )
35from tftp.backend import (39from tftp.backend import (
36 FilesystemSynchronousBackend,40 FilesystemSynchronousBackend,
37 IReader,41 IReader,
38 )42 )
39from tftp.errors import FileNotFound43from tftp.errors import FileNotFound
44from tftp.protocol import TFTP
45from twisted.application import internet
46from twisted.application.service import MultiService
40from twisted.python.context import get47from twisted.python.context import get
41from twisted.web.client import getPage48from twisted.web.client import getPage
42import twisted.web.error49import twisted.web.error
@@ -223,3 +230,72 @@
223 d = self.get_config_reader(params)230 d = self.get_config_reader(params)
224 d.addErrback(self.get_page_errback, file_name)231 d.addErrback(self.get_page_errback, file_name)
225 return d232 return d
233
234
235class TFTPService(MultiService, object):
236 """An umbrella service representing a set of running TFTP servers.
237
238 Creates a UDP server individually for each discovered network
239 interface, so that we can detect the interface via which we have
240 received a datagram.
241
242 It then periodically updates the servers running in case there's a
243 change to the host machine's network configuration.
244
245 :ivar backend: The :class:`TFTPBackend` being used to service TFTP
246 requests.
247
248 :ivar port: The port on which each server is started.
249
250 :ivar refresher: A :class:`TimerService` that calls
251 ``updateServers`` periodically.
252
253 """
254
255 def __init__(self, root, port, generator):
256 """
257 :param root: The root directory for this TFTP server.
258 :param port: The port on which each server should be started.
259 :param generator: The URL to be queried for PXE configuration.
260 """
261 super(TFTPService, self).__init__()
262 self.backend, self.port = TFTPBackend(root, generator), port
263 # Establish a periodic call to self.updateServers() every 45
264 # seconds, so that this service eventually converges on truth.
265 # TimerService ensures that a call is made to it's target
266 # function immediately as it's started, so there's no need to
267 # call updateServers() from here.
268 self.refresher = internet.TimerService(45, self.updateServers)
269 self.refresher.setName("refresher")
270 self.refresher.setServiceParent(self)
271
272 def getServers(self):
273 """Return a set of all configured servers.
274
275 :rtype: :class:`set` of :class:`internet.UDPServer`
276 """
277 return {
278 service for service in self
279 if service is not self.refresher
280 }
281
282 def updateServers(self):
283 """Run a server on every interface.
284
285 For each configured network interface this will start a TFTP
286 server. If called later it will bring up servers on newly
287 configured interfaces and bring down servers on deconfigured
288 interfaces.
289 """
290 addrs_established = set(service.name for service in self.getServers())
291 addrs_desired = set(get_all_interface_addresses())
292
293 for address in addrs_desired - addrs_established:
294 tftp_service = internet.UDPServer(
295 self.port, TFTP(self.backend), interface=address)
296 tftp_service.setName(address)
297 tftp_service.setServiceParent(self)
298
299 for address in addrs_established - addrs_desired:
300 tftp_service = self.getServiceNamed(address)
301 tftp_service.disownServiceParent()