Merge lp:~allenap/maas/tftp-rebind-on-netif-change into lp:~maas-committers/maas/trunk
- tftp-rebind-on-netif-change
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
Description of the change
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.
thread or outside.
>
> Anyway, lovely change.
Thanks for the review!
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
189 +
190 + def test_tftp_
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?
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
> 189 +
> 190 + def test_tftp_
>
> 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!
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.
Julian Edwards (julian-edwards) wrote : | # |
+ # TimerService ensures that a call is made to it's target
its*
Preview Diff
1 | === modified file 'services/pserv/run' |
2 | --- services/pserv/run 2013-08-30 09:20:25 +0000 |
3 | +++ services/pserv/run 2014-02-13 12:29:56 +0000 |
4 | @@ -21,6 +21,7 @@ |
5 | # Obtain the development setting for CLUSTER_UUID. |
6 | . etc/demo_maas_cluster.conf |
7 | export CLUSTER_UUID |
8 | +export MAAS_URL |
9 | |
10 | exec $(command -v authbind && echo --deep) \ |
11 | "${script}" --nodaemon --pidfile="" maas-pserv --config-file "${config}" |
12 | |
13 | === modified file 'src/provisioningserver/plugin.py' |
14 | --- src/provisioningserver/plugin.py 2014-02-10 17:38:08 +0000 |
15 | +++ src/provisioningserver/plugin.py 2014-02-13 12:29:56 +0000 |
16 | @@ -21,10 +21,7 @@ |
17 | LogService, |
18 | OOPSService, |
19 | ) |
20 | -from provisioningserver.tftp import TFTPBackend |
21 | -from provisioningserver.utils import get_all_interface_addresses |
22 | -from tftp.protocol import TFTP |
23 | -from twisted.application import internet |
24 | +from provisioningserver.tftp import TFTPService |
25 | from twisted.application.internet import ( |
26 | TCPClient, |
27 | TCPServer, |
28 | @@ -154,18 +151,9 @@ |
29 | |
30 | def _makeTFTPService(self, tftp_config): |
31 | """Create the dynamic TFTP service.""" |
32 | - backend = TFTPBackend(tftp_config["root"], tftp_config["generator"]) |
33 | - # Create a UDP server individually for each discovered network |
34 | - # interface, so that we can detect the interface via which we have |
35 | - # received a datagram. |
36 | - tftp_services = MultiService() |
37 | - tftp_services.setName("tftp") |
38 | - for address in get_all_interface_addresses(): |
39 | - tftp_service = internet.UDPServer( |
40 | - tftp_config["port"], TFTP(backend), interface=address) |
41 | - tftp_service.setName(address) |
42 | - tftp_service.setServiceParent(tftp_services) |
43 | - return tftp_services |
44 | + tftp_service = TFTPService(**tftp_config) |
45 | + tftp_service.setName("tftp") |
46 | + return tftp_service |
47 | |
48 | def _makeRPCService(self, rpc_config): |
49 | rpc_service = ClusterClientService(reactor) |
50 | |
51 | === modified file 'src/provisioningserver/tests/test_plugin.py' |
52 | --- src/provisioningserver/tests/test_plugin.py 2014-01-31 16:14:52 +0000 |
53 | +++ src/provisioningserver/tests/test_plugin.py 2014-02-13 12:29:56 +0000 |
54 | @@ -19,29 +19,29 @@ |
55 | |
56 | from maastesting.factory import factory |
57 | from maastesting.testcase import MAASTestCase |
58 | -from provisioningserver import plugin |
59 | from provisioningserver.plugin import ( |
60 | Options, |
61 | ProvisioningRealm, |
62 | ProvisioningServiceMaker, |
63 | SingleUsernamePasswordChecker, |
64 | ) |
65 | -from provisioningserver.tftp import TFTPBackend |
66 | +from provisioningserver.tftp import ( |
67 | + TFTPBackend, |
68 | + TFTPService, |
69 | + ) |
70 | from testtools.deferredruntest import ( |
71 | assert_fails_with, |
72 | AsynchronousDeferredRunTest, |
73 | ) |
74 | from testtools.matchers import ( |
75 | AfterPreprocessing, |
76 | - AllMatch, |
77 | Equals, |
78 | IsInstance, |
79 | MatchesAll, |
80 | MatchesException, |
81 | + MatchesStructure, |
82 | Raises, |
83 | ) |
84 | -from tftp.protocol import TFTP |
85 | -from twisted.application.internet import UDPServer |
86 | from twisted.application.service import MultiService |
87 | from twisted.cred.credentials import UsernamePassword |
88 | from twisted.cred.error import UnauthorizedLogin |
89 | @@ -128,13 +128,6 @@ |
90 | |
91 | def test_tftp_service(self): |
92 | # A TFTP service is configured and added to the top-level service. |
93 | - interfaces = [ |
94 | - factory.getRandomIPAddress(), |
95 | - factory.getRandomIPAddress(), |
96 | - ] |
97 | - self.patch( |
98 | - plugin, "get_all_interface_addresses", |
99 | - lambda: interfaces) |
100 | config = { |
101 | "tftp": { |
102 | "generator": "http://candlemass/solitude", |
103 | @@ -146,14 +139,9 @@ |
104 | options["config-file"] = self.write_config(config) |
105 | service_maker = ProvisioningServiceMaker("Harry", "Hill") |
106 | service = service_maker.makeService(options) |
107 | - tftp_services = service.getServiceNamed("tftp") |
108 | - # The "tftp" service is a multi-service containing UDP servers for |
109 | - # each interface defined by get_all_interface_addresses(). |
110 | - self.assertIsInstance(tftp_services, MultiService) |
111 | - services = [ |
112 | - tftp_services.getServiceNamed(interface) |
113 | - for interface in interfaces |
114 | - ] |
115 | + tftp_service = service.getServiceNamed("tftp") |
116 | + self.assertIsInstance(tftp_service, TFTPService) |
117 | + |
118 | expected_backend = MatchesAll( |
119 | IsInstance(TFTPBackend), |
120 | AfterPreprocessing( |
121 | @@ -162,27 +150,12 @@ |
122 | AfterPreprocessing( |
123 | lambda backend: backend.generator_url.geturl(), |
124 | Equals(config["tftp"]["generator"]))) |
125 | - expected_protocol = MatchesAll( |
126 | - IsInstance(TFTP), |
127 | - AfterPreprocessing( |
128 | - lambda protocol: protocol.backend, |
129 | - expected_backend)) |
130 | - expected_service = MatchesAll( |
131 | - IsInstance(UDPServer), |
132 | - AfterPreprocessing( |
133 | - lambda service: len(service.args), |
134 | - Equals(2)), |
135 | - AfterPreprocessing( |
136 | - lambda service: service.args[0], # port |
137 | - Equals(config["tftp"]["port"])), |
138 | - AfterPreprocessing( |
139 | - lambda service: service.args[1], # protocol |
140 | - expected_protocol)) |
141 | - self.assertThat(services, AllMatch(expected_service)) |
142 | - # Only the interface used for each service differs. |
143 | - self.assertEqual( |
144 | - [svc.kwargs for svc in services], |
145 | - [{"interface": interface} for interface in interfaces]) |
146 | + |
147 | + self.assertThat( |
148 | + tftp_service, MatchesStructure( |
149 | + backend=expected_backend, |
150 | + port=Equals(config["tftp"]["port"]), |
151 | + )) |
152 | |
153 | |
154 | class TestSingleUsernamePasswordChecker(MAASTestCase): |
155 | |
156 | === modified file 'src/provisioningserver/tests/test_tftp.py' |
157 | --- src/provisioningserver/tests/test_tftp.py 2013-10-15 11:35:10 +0000 |
158 | +++ src/provisioningserver/tests/test_tftp.py 2014-02-13 12:29:56 +0000 |
159 | @@ -31,9 +31,22 @@ |
160 | from provisioningserver.tftp import ( |
161 | BytesReader, |
162 | TFTPBackend, |
163 | + TFTPService, |
164 | ) |
165 | from testtools.deferredruntest import AsynchronousDeferredRunTest |
166 | +from testtools.matchers import ( |
167 | + AfterPreprocessing, |
168 | + AllMatch, |
169 | + Equals, |
170 | + IsInstance, |
171 | + MatchesAll, |
172 | + MatchesStructure, |
173 | + ) |
174 | from tftp.backend import IReader |
175 | +from tftp.protocol import TFTP |
176 | +from twisted.application import internet |
177 | +from twisted.application.service import MultiService |
178 | +from twisted.internet import reactor |
179 | from twisted.internet.defer import ( |
180 | inlineCallbacks, |
181 | succeed, |
182 | @@ -288,3 +301,97 @@ |
183 | self.assertEqual(fake_render_result.encode("utf-8"), output) |
184 | backend.render_pxe_config.assert_called_once_with( |
185 | kernel_params=fake_kernel_params, **fake_params) |
186 | + |
187 | + |
188 | +class TestTFTPService(MAASTestCase): |
189 | + |
190 | + def test_tftp_service(self): |
191 | + # A TFTP service is configured and added to the top-level service. |
192 | + interfaces = [ |
193 | + factory.getRandomIPAddress(), |
194 | + factory.getRandomIPAddress(), |
195 | + ] |
196 | + self.patch( |
197 | + tftp_module, "get_all_interface_addresses", |
198 | + lambda: interfaces) |
199 | + example_root = self.make_dir() |
200 | + example_generator = "http://example.com/generator" |
201 | + example_port = factory.getRandomPort() |
202 | + tftp_service = TFTPService( |
203 | + root=example_root, generator=example_generator, |
204 | + port=example_port) |
205 | + tftp_service.updateServers() |
206 | + # The "tftp" service is a multi-service containing UDP servers for |
207 | + # each interface defined by get_all_interface_addresses(). |
208 | + self.assertIsInstance(tftp_service, MultiService) |
209 | + # There's also a TimerService that updates the servers every 45s. |
210 | + self.assertThat( |
211 | + tftp_service.refresher, MatchesStructure.byEquality( |
212 | + step=45, parent=tftp_service, name="refresher", |
213 | + call=(tftp_service.updateServers, (), {}), |
214 | + )) |
215 | + expected_backend = MatchesAll( |
216 | + IsInstance(TFTPBackend), |
217 | + AfterPreprocessing( |
218 | + lambda backend: backend.base.path, |
219 | + Equals(example_root)), |
220 | + AfterPreprocessing( |
221 | + lambda backend: backend.generator_url.geturl(), |
222 | + Equals(example_generator))) |
223 | + expected_protocol = MatchesAll( |
224 | + IsInstance(TFTP), |
225 | + AfterPreprocessing( |
226 | + lambda protocol: protocol.backend, |
227 | + expected_backend)) |
228 | + expected_server = MatchesAll( |
229 | + IsInstance(internet.UDPServer), |
230 | + AfterPreprocessing( |
231 | + lambda service: len(service.args), |
232 | + Equals(2)), |
233 | + AfterPreprocessing( |
234 | + lambda service: service.args[0], # port |
235 | + Equals(example_port)), |
236 | + AfterPreprocessing( |
237 | + lambda service: service.args[1], # protocol |
238 | + expected_protocol)) |
239 | + self.assertThat( |
240 | + tftp_service.getServers(), |
241 | + AllMatch(expected_server)) |
242 | + # Only the interface used for each service differs. |
243 | + self.assertItemsEqual( |
244 | + [svc.kwargs for svc in tftp_service.getServers()], |
245 | + [{"interface": interface} for interface in interfaces]) |
246 | + |
247 | + def test_tftp_service_rebinds_on_HUP(self): |
248 | + # Initial set of interfaces to bind to. |
249 | + interfaces = {"1.1.1.1", "2.2.2.2"} |
250 | + self.patch( |
251 | + tftp_module, "get_all_interface_addresses", |
252 | + lambda: interfaces) |
253 | + |
254 | + tftp_service = TFTPService( |
255 | + root=self.make_dir(), generator="http://mighty/wind", |
256 | + port=factory.getRandomPort()) |
257 | + tftp_service.updateServers() |
258 | + |
259 | + # The child services of tftp_services are named after the |
260 | + # interface they bind to. |
261 | + self.assertEqual(interfaces, { |
262 | + server.name for server in tftp_service.getServers() |
263 | + }) |
264 | + |
265 | + # Update the set of interfaces to bind to. |
266 | + interfaces.add("3.3.3.3") |
267 | + interfaces.remove("1.1.1.1") |
268 | + |
269 | + # Ask the TFTP service to update its set of servers. |
270 | + tftp_service.updateServers() |
271 | + |
272 | + # We're in the reactor thread but we want to move the reactor |
273 | + # forwards, hence we need to get all explicit about it. |
274 | + reactor.runUntilCurrent() |
275 | + |
276 | + # The interfaces now bound match the updated interfaces set. |
277 | + self.assertEqual(interfaces, { |
278 | + server.name for server in tftp_service.getServers() |
279 | + }) |
280 | |
281 | === modified file 'src/provisioningserver/tftp.py' |
282 | --- src/provisioningserver/tftp.py 2013-10-15 11:35:10 +0000 |
283 | +++ src/provisioningserver/tftp.py 2014-02-13 12:29:56 +0000 |
284 | @@ -14,6 +14,7 @@ |
285 | __metaclass__ = type |
286 | __all__ = [ |
287 | "TFTPBackend", |
288 | + "TFTPService", |
289 | ] |
290 | |
291 | import httplib |
292 | @@ -31,12 +32,18 @@ |
293 | from provisioningserver.enum import ARP_HTYPE |
294 | from provisioningserver.kernel_opts import KernelParameters |
295 | from provisioningserver.pxe.config import render_pxe_config |
296 | -from provisioningserver.utils import deferred |
297 | +from provisioningserver.utils import ( |
298 | + deferred, |
299 | + get_all_interface_addresses, |
300 | + ) |
301 | from tftp.backend import ( |
302 | FilesystemSynchronousBackend, |
303 | IReader, |
304 | ) |
305 | from tftp.errors import FileNotFound |
306 | +from tftp.protocol import TFTP |
307 | +from twisted.application import internet |
308 | +from twisted.application.service import MultiService |
309 | from twisted.python.context import get |
310 | from twisted.web.client import getPage |
311 | import twisted.web.error |
312 | @@ -223,3 +230,72 @@ |
313 | d = self.get_config_reader(params) |
314 | d.addErrback(self.get_page_errback, file_name) |
315 | return d |
316 | + |
317 | + |
318 | +class TFTPService(MultiService, object): |
319 | + """An umbrella service representing a set of running TFTP servers. |
320 | + |
321 | + Creates a UDP server individually for each discovered network |
322 | + interface, so that we can detect the interface via which we have |
323 | + received a datagram. |
324 | + |
325 | + It then periodically updates the servers running in case there's a |
326 | + change to the host machine's network configuration. |
327 | + |
328 | + :ivar backend: The :class:`TFTPBackend` being used to service TFTP |
329 | + requests. |
330 | + |
331 | + :ivar port: The port on which each server is started. |
332 | + |
333 | + :ivar refresher: A :class:`TimerService` that calls |
334 | + ``updateServers`` periodically. |
335 | + |
336 | + """ |
337 | + |
338 | + def __init__(self, root, port, generator): |
339 | + """ |
340 | + :param root: The root directory for this TFTP server. |
341 | + :param port: The port on which each server should be started. |
342 | + :param generator: The URL to be queried for PXE configuration. |
343 | + """ |
344 | + super(TFTPService, self).__init__() |
345 | + self.backend, self.port = TFTPBackend(root, generator), port |
346 | + # Establish a periodic call to self.updateServers() every 45 |
347 | + # seconds, so that this service eventually converges on truth. |
348 | + # TimerService ensures that a call is made to it's target |
349 | + # function immediately as it's started, so there's no need to |
350 | + # call updateServers() from here. |
351 | + self.refresher = internet.TimerService(45, self.updateServers) |
352 | + self.refresher.setName("refresher") |
353 | + self.refresher.setServiceParent(self) |
354 | + |
355 | + def getServers(self): |
356 | + """Return a set of all configured servers. |
357 | + |
358 | + :rtype: :class:`set` of :class:`internet.UDPServer` |
359 | + """ |
360 | + return { |
361 | + service for service in self |
362 | + if service is not self.refresher |
363 | + } |
364 | + |
365 | + def updateServers(self): |
366 | + """Run a server on every interface. |
367 | + |
368 | + For each configured network interface this will start a TFTP |
369 | + server. If called later it will bring up servers on newly |
370 | + configured interfaces and bring down servers on deconfigured |
371 | + interfaces. |
372 | + """ |
373 | + addrs_established = set(service.name for service in self.getServers()) |
374 | + addrs_desired = set(get_all_interface_addresses()) |
375 | + |
376 | + for address in addrs_desired - addrs_established: |
377 | + tftp_service = internet.UDPServer( |
378 | + self.port, TFTP(self.backend), interface=address) |
379 | + tftp_service.setName(address) |
380 | + tftp_service.setServiceParent(self) |
381 | + |
382 | + for address in addrs_established - addrs_desired: |
383 | + tftp_service = self.getServiceNamed(address) |
384 | + tftp_service.disownServiceParent() |
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.