Merge lp:~julian-edwards/maas/pserv-services-dir into lp:~maas-committers/maas/trunk
- pserv-services-dir
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Julian Edwards |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2827 |
Proposed branch: | lp:~julian-edwards/maas/pserv-services-dir |
Merge into: | lp:~maas-committers/maas/trunk |
Diff against target: |
1068 lines (+499/-414) 10 files modified
src/provisioningserver/boot/tests/test_powernv.py (+1/-1) src/provisioningserver/plugin.py (+7/-3) src/provisioningserver/pserv_services/image_download_service.py (+120/-0) src/provisioningserver/pserv_services/node_power_monitor_service.py (+75/-0) src/provisioningserver/pserv_services/tests/test_image_download_service.py (+279/-0) src/provisioningserver/rpc/boot_images.py (+2/-103) src/provisioningserver/rpc/power.py (+3/-49) src/provisioningserver/rpc/tests/test_boot_images.py (+2/-252) src/provisioningserver/tests/test_plugin.py (+7/-3) src/provisioningserver/tests/test_tftp.py (+3/-3) |
To merge this branch: | bzr merge lp:~julian-edwards/maas/pserv-services-dir |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jeroen T. Vermeulen (community) | Approve | ||
Review via email: mp+232344@code.launchpad.net |
Commit message
Create a provisioningser
Description of the change
This moves a load of scattered pserv services into a single directory called pserv_services. It would have just been called services, however provisioningser
There is one service left to migrate, ClusterClientSe
Julian Edwards (julian-edwards) wrote : | # |
Jeroen T. Vermeulen (jtv) wrote : | # |
Your revulsion at the __init__.py services situation is noted. Now please drop that README file and just live with the new name!
(For the record, what bugs me most about the new name is probably that the "serv" and the "services" repeat the same root.)
.
For the XXX in node_power_
.
I see you preserved the "one_week_ago = er, 15 minutes ago" sickness. That makes my review easier (no need to look for changes in the moved code) but I hope we'll get rid of that soon as well.
Julian Edwards (julian-edwards) wrote : | # |
Thank you!
On Wednesday 27 Aug 2014 04:02:05 you wrote:
> Review: Approve
>
> Your revulsion at the __init__.py services situation is noted. Now please
> drop that README file and just live with the new name!
I wrote that in a fit of pique. I'll get rid of it.
> (For the record, what bugs me most about the new name is probably that the
> "serv" and the "services" repeat the same root.)
Yes. All of the yes.
> For the XXX in node_power_
> include its number in the comment?
Yes. I forgot to do that.
> I see you preserved the "one_week_ago = er, 15 minutes ago" sickness. That
> makes my review easier (no need to look for changes in the moved code) but
> I hope we'll get rid of that soon as well.
Yeah deliberately so, I didn't want to touch the code at all. The next change
can fix it, pending my questions about it in Blake's last merge.
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~julian-edwards/maas/pserv-services-dir into lp:maas failed. Below is the output from the failed tests.
Ign http://
Ign http://
Get:1 http://
Ign http://
Get:2 http://
Hit http://
Get:3 http://
Hit http://
Get:4 http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Get:5 http://
Get:6 http://
Get:7 http://
Get:8 http://
Hit http://
Hit http://
Get:9 http://
Get:10 http://
Get:11 http://
Get:12 http://
Hit http://
Hit http://
Ign http://
Ign http://
Fetched 1,032 kB in 0s (1,628 kB/s)
Reading package lists...
sudo DEBIAN_
--
Julian Edwards (julian-edwards) wrote : | # |
Seems like you now have to wait for the branch scanner to catch up with the approved revno before you can mark it approved. :(
MAAS Lander (maas-lander) wrote : | # |
The attempt to merge lp:~julian-edwards/maas/pserv-services-dir into lp:maas failed. Below is the output from the failed tests.
Ign http://
Hit http://
Hit http://
Ign http://
Ign http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Hit http://
Ign http://
Ign http://
Reading package lists...
sudo DEBIAN_
--
Julian Edwards (julian-edwards) wrote : | # |
Sigh.
Julian Edwards (julian-edwards) wrote : | # |
Forgot to move the tftp test, re-landing.
Preview Diff
1 | === modified file 'src/provisioningserver/boot/tests/test_powernv.py' |
2 | --- src/provisioningserver/boot/tests/test_powernv.py 2014-08-13 21:49:35 +0000 |
3 | +++ src/provisioningserver/boot/tests/test_powernv.py 2014-08-27 06:01:25 +0000 |
4 | @@ -31,9 +31,9 @@ |
5 | ) |
6 | from provisioningserver.boot.tests.test_pxe import parse_pxe_config |
7 | from provisioningserver.boot.tftppath import compose_image_path |
8 | +from provisioningserver.pserv_services.tftp import TFTPBackend |
9 | from provisioningserver.testing.config import set_tftp_root |
10 | from provisioningserver.tests.test_kernel_opts import make_kernel_parameters |
11 | -from provisioningserver.tftp import TFTPBackend |
12 | from testtools.matchers import ( |
13 | IsInstance, |
14 | MatchesAll, |
15 | |
16 | === modified file 'src/provisioningserver/plugin.py' |
17 | --- src/provisioningserver/plugin.py 2014-08-26 23:55:04 +0000 |
18 | +++ src/provisioningserver/plugin.py 2014-08-27 06:01:25 +0000 |
19 | @@ -31,10 +31,14 @@ |
20 | import provisioningserver |
21 | from provisioningserver.cluster_config import get_cluster_uuid |
22 | from provisioningserver.config import Config |
23 | -from provisioningserver.rpc.boot_images import PeriodicImageDownloadService |
24 | +from provisioningserver.pserv_services.image_download_service import ( |
25 | + PeriodicImageDownloadService, |
26 | + ) |
27 | +from provisioningserver.pserv_services.node_power_monitor_service import ( |
28 | + NodePowerMonitorService, |
29 | + ) |
30 | +from provisioningserver.pserv_services.tftp import TFTPService |
31 | from provisioningserver.rpc.clusterservice import ClusterClientService |
32 | -from provisioningserver.rpc.power import NodePowerMonitorService |
33 | -from provisioningserver.tftp import TFTPService |
34 | from twisted.application.internet import TCPServer |
35 | from twisted.application.service import ( |
36 | IServiceMaker, |
37 | |
38 | === added directory 'src/provisioningserver/pserv_services' |
39 | === added file 'src/provisioningserver/pserv_services/__init__.py' |
40 | === added file 'src/provisioningserver/pserv_services/image_download_service.py' |
41 | --- src/provisioningserver/pserv_services/image_download_service.py 1970-01-01 00:00:00 +0000 |
42 | +++ src/provisioningserver/pserv_services/image_download_service.py 2014-08-27 06:01:25 +0000 |
43 | @@ -0,0 +1,120 @@ |
44 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
45 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
46 | + |
47 | +"""Service to periodically refresh the boot images.""" |
48 | + |
49 | +from __future__ import ( |
50 | + absolute_import, |
51 | + print_function, |
52 | + unicode_literals, |
53 | + ) |
54 | + |
55 | +str = None |
56 | + |
57 | +__metaclass__ = type |
58 | +__all__ = [ |
59 | + "PeriodicImageDownloadService", |
60 | + ] |
61 | + |
62 | + |
63 | +from datetime import timedelta |
64 | + |
65 | +from provisioningserver.boot import tftppath |
66 | +from provisioningserver.logger import get_maas_logger |
67 | +from provisioningserver.rpc.boot_images import import_boot_images |
68 | +from provisioningserver.rpc.exceptions import NoConnectionsAvailable |
69 | +from provisioningserver.rpc.region import ( |
70 | + GetBootSources, |
71 | + GetBootSourcesV2, |
72 | + ) |
73 | +from provisioningserver.utils.twisted import pause |
74 | +from twisted.application.internet import TimerService |
75 | +from twisted.internet.defer import ( |
76 | + DeferredLock, |
77 | + inlineCallbacks, |
78 | + returnValue, |
79 | + ) |
80 | +from twisted.spread.pb import NoSuchMethod |
81 | + |
82 | + |
83 | +maaslog = get_maas_logger("boot_image_download_service") |
84 | +service_lock = DeferredLock() |
85 | + |
86 | + |
87 | +class PeriodicImageDownloadService(TimerService, object): |
88 | + """Twisted service to periodically refresh ephemeral images. |
89 | + |
90 | + :param client_service: A `ClusterClientService` instance for talking |
91 | + to the region controller. |
92 | + :param reactor: An `IReactor` instance. |
93 | + """ |
94 | + |
95 | + check_interval = timedelta(minutes=5).total_seconds() |
96 | + |
97 | + def __init__(self, client_service, reactor, cluster_uuid): |
98 | + # Call self.check() every self.check_interval. |
99 | + super(PeriodicImageDownloadService, self).__init__( |
100 | + self.check_interval, self.maybe_start_download) |
101 | + self.clock = reactor |
102 | + self.client_service = client_service |
103 | + self.uuid = cluster_uuid |
104 | + |
105 | + @inlineCallbacks |
106 | + def _get_boot_sources(self, client): |
107 | + """Gets the boot sources from the region.""" |
108 | + try: |
109 | + sources = yield client(GetBootSourcesV2, uuid=self.uuid) |
110 | + except NoSuchMethod: |
111 | + # Region has not been upgraded to support the new call, use the |
112 | + # old call. The old call did not provide the new os selection |
113 | + # parameter. Region does not support boot source selection by os, |
114 | + # so its set too allow all operating systems. |
115 | + sources = yield client(GetBootSources, uuid=self.uuid) |
116 | + for source in sources['sources']: |
117 | + for selection in source['selections']: |
118 | + selection['os'] = '*' |
119 | + returnValue(sources) |
120 | + |
121 | + @inlineCallbacks |
122 | + def _start_download(self): |
123 | + client = None |
124 | + # Retry a few times, since this service usually comes up before |
125 | + # the RPC service. |
126 | + for _ in range(3): |
127 | + try: |
128 | + client = self.client_service.getClient() |
129 | + break |
130 | + except NoConnectionsAvailable: |
131 | + yield pause(5) |
132 | + if client is None: |
133 | + maaslog.error( |
134 | + "Can't initiate image download, no RPC connection to region.") |
135 | + return |
136 | + |
137 | + # Get sources from region |
138 | + sources = yield self._get_boot_sources(client) |
139 | + yield import_boot_images(sources.get("sources")) |
140 | + |
141 | + @inlineCallbacks |
142 | + def maybe_start_download(self): |
143 | + """Check the time the last image refresh happened and initiate a new |
144 | + one if older than 15 minutes. |
145 | + """ |
146 | + # Use a DeferredLock to prevent simultaneous downloads. |
147 | + if service_lock.locked: |
148 | + # Don't want to block on lock release. |
149 | + return |
150 | + yield service_lock.acquire() |
151 | + try: |
152 | + last_modified = tftppath.maas_meta_last_modified() |
153 | + if last_modified is None: |
154 | + # Don't auto-refresh if the user has never manually initiated |
155 | + # a download. |
156 | + return |
157 | + |
158 | + age_in_seconds = self.clock.seconds() - last_modified |
159 | + if age_in_seconds >= timedelta(minutes=15).total_seconds(): |
160 | + yield self._start_download() |
161 | + |
162 | + finally: |
163 | + service_lock.release() |
164 | |
165 | === added file 'src/provisioningserver/pserv_services/node_power_monitor_service.py' |
166 | --- src/provisioningserver/pserv_services/node_power_monitor_service.py 1970-01-01 00:00:00 +0000 |
167 | +++ src/provisioningserver/pserv_services/node_power_monitor_service.py 2014-08-27 06:01:25 +0000 |
168 | @@ -0,0 +1,75 @@ |
169 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
170 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
171 | + |
172 | +"""Service to periodically query the power state on this cluster's nodes.""" |
173 | + |
174 | + |
175 | +from __future__ import ( |
176 | + absolute_import, |
177 | + print_function, |
178 | + unicode_literals, |
179 | + ) |
180 | + |
181 | +str = None |
182 | + |
183 | +__metaclass__ = type |
184 | +__all__ = [ |
185 | + "NodePowerMonitorService" |
186 | +] |
187 | + |
188 | + |
189 | +from provisioningserver.logger.log import get_maas_logger |
190 | +from provisioningserver.rpc.exceptions import NoConnectionsAvailable |
191 | +from provisioningserver.rpc.power import query_all_nodes |
192 | +from provisioningserver.rpc.region import ListNodePowerParameters |
193 | +from provisioningserver.utils.twisted import pause |
194 | +from twisted.application.internet import TimerService |
195 | +from twisted.internet.defer import inlineCallbacks |
196 | + |
197 | + |
198 | +maaslog = get_maas_logger("power_monitor_service") |
199 | + |
200 | + |
201 | +class NodePowerMonitorService(TimerService, object): |
202 | + """Twisted service to monitor the status of all nodes |
203 | + controlled by this cluster. |
204 | + |
205 | + :param client_service: A `ClusterClientService` instance for talking |
206 | + to the region controller. |
207 | + :param reactor: An `IReactor` instance. |
208 | + """ |
209 | + |
210 | + # XXX 2014-08-27 bug=1361967 |
211 | + # This service is COMPLETELY UNTESTED. |
212 | + |
213 | + check_interval = 600 # 5 minutes. |
214 | + |
215 | + def __init__(self, client_service, reactor, cluster_uuid): |
216 | + # Call self.check() every self.check_interval. |
217 | + super(NodePowerMonitorService, self).__init__( |
218 | + self.check_interval, self.query_nodes) |
219 | + self.clock = reactor |
220 | + self.client_service = client_service |
221 | + self.uuid = cluster_uuid |
222 | + |
223 | + @inlineCallbacks |
224 | + def query_nodes(self): |
225 | + client = None |
226 | + # Retry a few times, since this service usually comes up before |
227 | + # the RPC service. |
228 | + for _ in range(3): |
229 | + try: |
230 | + client = self.client_service.getClient() |
231 | + break |
232 | + except NoConnectionsAvailable: |
233 | + yield pause(5) |
234 | + if client is None: |
235 | + maaslog.error( |
236 | + "Can't query nodes's BMC for power state, no RPC connection " |
237 | + "to region.") |
238 | + return |
239 | + |
240 | + # Get the nodes from the Region |
241 | + response = yield client(ListNodePowerParameters, uuid=self.uuid) |
242 | + nodes = response['nodes'] |
243 | + yield query_all_nodes(nodes) |
244 | |
245 | === added directory 'src/provisioningserver/pserv_services/tests' |
246 | === added file 'src/provisioningserver/pserv_services/tests/__init__.py' |
247 | === added file 'src/provisioningserver/pserv_services/tests/test_image_download_service.py' |
248 | --- src/provisioningserver/pserv_services/tests/test_image_download_service.py 1970-01-01 00:00:00 +0000 |
249 | +++ src/provisioningserver/pserv_services/tests/test_image_download_service.py 2014-08-27 06:01:25 +0000 |
250 | @@ -0,0 +1,279 @@ |
251 | +# Copyright 2014 Canonical Ltd. This software is licensed under the |
252 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
253 | + |
254 | +"""Tests for provisioningserver.pserv_services.image_download_service""" |
255 | + |
256 | +from __future__ import ( |
257 | + absolute_import, |
258 | + print_function, |
259 | + unicode_literals, |
260 | + ) |
261 | + |
262 | +str = None |
263 | + |
264 | +__metaclass__ = type |
265 | +__all__ = [] |
266 | + |
267 | + |
268 | +from datetime import timedelta |
269 | + |
270 | +from maastesting.factory import factory |
271 | +from maastesting.matchers import ( |
272 | + get_mock_calls, |
273 | + MockCalledOnceWith, |
274 | + MockCallsMatch, |
275 | + MockNotCalled, |
276 | + ) |
277 | +from mock import ( |
278 | + call, |
279 | + Mock, |
280 | + sentinel, |
281 | + ) |
282 | +from provisioningserver.boot import tftppath |
283 | +from provisioningserver.pserv_services.image_download_service import ( |
284 | + PeriodicImageDownloadService, |
285 | + service_lock, |
286 | + ) |
287 | +from provisioningserver.rpc import boot_images |
288 | +from provisioningserver.rpc.boot_images import _run_import |
289 | +from provisioningserver.rpc.exceptions import NoConnectionsAvailable |
290 | +from provisioningserver.rpc.region import ( |
291 | + GetBootSources, |
292 | + GetBootSourcesV2, |
293 | + ) |
294 | +from provisioningserver.testing.testcase import PservTestCase |
295 | +from provisioningserver.utils.twisted import pause |
296 | +from testtools.deferredruntest import AsynchronousDeferredRunTest |
297 | +from twisted.application.internet import TimerService |
298 | +from twisted.internet import defer |
299 | +from twisted.internet.task import Clock |
300 | +from twisted.spread.pb import NoSuchMethod |
301 | + |
302 | + |
303 | +class TestPeriodicImageDownloadService(PservTestCase): |
304 | + |
305 | + run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5) |
306 | + |
307 | + def test_init(self): |
308 | + service = PeriodicImageDownloadService( |
309 | + sentinel.service, sentinel.clock, sentinel.uuid) |
310 | + self.assertIsInstance(service, TimerService) |
311 | + self.assertIs(service.clock, sentinel.clock) |
312 | + self.assertIs(service.uuid, sentinel.uuid) |
313 | + self.assertIs(service.client_service, sentinel.service) |
314 | + |
315 | + def patch_download(self, service, return_value): |
316 | + patched = self.patch(service, '_start_download') |
317 | + patched.return_value = defer.succeed(return_value) |
318 | + return patched |
319 | + |
320 | + def test_is_called_every_interval(self): |
321 | + clock = Clock() |
322 | + service = PeriodicImageDownloadService( |
323 | + sentinel.service, clock, sentinel.uuid) |
324 | + # Avoid actual downloads: |
325 | + self.patch_download(service, None) |
326 | + maas_meta_last_modified = self.patch( |
327 | + tftppath, 'maas_meta_last_modified') |
328 | + maas_meta_last_modified.return_value = None |
329 | + service.startService() |
330 | + |
331 | + # The first call is issued at startup. |
332 | + self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified))) |
333 | + |
334 | + # Wind clock forward one second less than the desired interval. |
335 | + clock.advance(service.check_interval - 1) |
336 | + # No more periodic calls made. |
337 | + self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified))) |
338 | + |
339 | + # Wind clock forward one second, past the interval. |
340 | + clock.advance(1) |
341 | + |
342 | + # Now there were two calls. |
343 | + self.assertEqual(2, len(get_mock_calls(maas_meta_last_modified))) |
344 | + |
345 | + # Forward another interval, should be three calls. |
346 | + clock.advance(service.check_interval) |
347 | + self.assertEqual(3, len(get_mock_calls(maas_meta_last_modified))) |
348 | + |
349 | + def test_no_download_if_no_meta_file(self): |
350 | + clock = Clock() |
351 | + service = PeriodicImageDownloadService( |
352 | + sentinel.service, clock, sentinel.uuid) |
353 | + _start_download = self.patch_download(service, None) |
354 | + self.patch( |
355 | + tftppath, |
356 | + 'maas_meta_last_modified').return_value = None |
357 | + service.startService() |
358 | + self.assertThat(_start_download, MockNotCalled()) |
359 | + |
360 | + def test_initiates_download_if_15_minutes_has_passed(self): |
361 | + clock = Clock() |
362 | + service = PeriodicImageDownloadService( |
363 | + sentinel.service, clock, sentinel.uuid) |
364 | + _start_download = self.patch_download(service, None) |
365 | + one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds() |
366 | + self.patch( |
367 | + tftppath, |
368 | + 'maas_meta_last_modified').return_value = one_week_ago |
369 | + service.startService() |
370 | + self.assertThat(_start_download, MockCalledOnceWith()) |
371 | + |
372 | + def test_no_download_if_15_minutes_has_not_passed(self): |
373 | + clock = Clock() |
374 | + service = PeriodicImageDownloadService( |
375 | + sentinel.service, clock, sentinel.uuid) |
376 | + _start_download = self.patch_download(service, None) |
377 | + one_week = timedelta(minutes=15).total_seconds() |
378 | + self.patch( |
379 | + tftppath, |
380 | + 'maas_meta_last_modified').return_value = clock.seconds() |
381 | + clock.advance(one_week - 1) |
382 | + service.startService() |
383 | + self.assertThat(_start_download, MockNotCalled()) |
384 | + |
385 | + def test_download_is_initiated_in_new_thread(self): |
386 | + clock = Clock() |
387 | + maas_meta_last_modified = self.patch( |
388 | + tftppath, 'maas_meta_last_modified') |
389 | + one_week = timedelta(minutes=15).total_seconds() |
390 | + maas_meta_last_modified.return_value = clock.seconds() - one_week |
391 | + rpc_client = Mock() |
392 | + client_call = Mock() |
393 | + client_call.side_effect = [ |
394 | + defer.succeed(dict(sources=sentinel.sources)), |
395 | + ] |
396 | + rpc_client.getClient.return_value = client_call |
397 | + |
398 | + # We could patch out 'import_boot_images' instead here but I |
399 | + # don't do that for 2 reasons: |
400 | + # 1. It requires spinning the reactor again before being able to |
401 | + # test the result. |
402 | + # 2. It means there's no thread to clean up after the test. |
403 | + deferToThread = self.patch(boot_images, 'deferToThread') |
404 | + deferToThread.return_value = defer.succeed(None) |
405 | + service = PeriodicImageDownloadService( |
406 | + rpc_client, clock, sentinel.uuid) |
407 | + service.startService() |
408 | + self.assertThat( |
409 | + deferToThread, MockCalledOnceWith( |
410 | + _run_import, sentinel.sources)) |
411 | + |
412 | + def test_no_download_if_no_rpc_connections(self): |
413 | + rpc_client = Mock() |
414 | + failure = NoConnectionsAvailable() |
415 | + rpc_client.getClient.return_value.side_effect = failure |
416 | + |
417 | + deferToThread = self.patch(boot_images, 'deferToThread') |
418 | + service = PeriodicImageDownloadService( |
419 | + rpc_client, Clock(), sentinel.uuid) |
420 | + service.startService() |
421 | + self.assertThat(deferToThread, MockNotCalled()) |
422 | + |
423 | + @defer.inlineCallbacks |
424 | + def test_does_not_run_if_lock_taken(self): |
425 | + maas_meta_last_modified = self.patch( |
426 | + tftppath, 'maas_meta_last_modified') |
427 | + yield service_lock.acquire() |
428 | + self.addCleanup(service_lock.release) |
429 | + service = PeriodicImageDownloadService( |
430 | + sentinel.rpc, Clock(), sentinel.uuid) |
431 | + service.startService() |
432 | + self.assertThat(maas_meta_last_modified, MockNotCalled()) |
433 | + |
434 | + def test_takes_lock_when_running(self): |
435 | + clock = Clock() |
436 | + service = PeriodicImageDownloadService( |
437 | + sentinel.rpc, clock, sentinel.uuid) |
438 | + |
439 | + # Patch the download func so it's just a Deferred that waits for |
440 | + # one second. |
441 | + _start_download = self.patch(service, '_start_download') |
442 | + _start_download.return_value = pause(1, clock) |
443 | + |
444 | + # Set conditions for a required download: |
445 | + one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds() |
446 | + self.patch( |
447 | + tftppath, |
448 | + 'maas_meta_last_modified').return_value = one_week_ago |
449 | + |
450 | + # Lock is acquired for the first download after startup. |
451 | + service.startService() |
452 | + self.assertTrue(service_lock.locked) |
453 | + |
454 | + # Lock is released once the download is done. |
455 | + clock.advance(1) |
456 | + self.assertFalse(service_lock.locked) |
457 | + |
458 | + @defer.inlineCallbacks |
459 | + def test__get_boot_sources_calls_get_boot_sources_v2_before_v1(self): |
460 | + clock = Clock() |
461 | + client_call = Mock() |
462 | + client_call.side_effect = [ |
463 | + defer.succeed(dict(sources=sentinel.sources)), |
464 | + ] |
465 | + |
466 | + service = PeriodicImageDownloadService( |
467 | + sentinel.rpc, clock, sentinel.uuid) |
468 | + sources = yield service._get_boot_sources(client_call) |
469 | + self.assertEqual(sources.get('sources'), sentinel.sources) |
470 | + self.assertThat( |
471 | + client_call, |
472 | + MockCalledOnceWith(GetBootSourcesV2, uuid=sentinel.uuid)) |
473 | + |
474 | + @defer.inlineCallbacks |
475 | + def test__get_boot_sources_calls_get_boot_sources_v1_on_v2_missing(self): |
476 | + clock = Clock() |
477 | + client_call = Mock() |
478 | + client_call.side_effect = [ |
479 | + defer.fail(NoSuchMethod()), |
480 | + defer.succeed(dict(sources=[])), |
481 | + ] |
482 | + |
483 | + service = PeriodicImageDownloadService( |
484 | + sentinel.rpc, clock, sentinel.uuid) |
485 | + yield service._get_boot_sources(client_call) |
486 | + self.assertThat( |
487 | + client_call, |
488 | + MockCallsMatch( |
489 | + call(GetBootSourcesV2, uuid=sentinel.uuid), |
490 | + call(GetBootSources, uuid=sentinel.uuid))) |
491 | + |
492 | + @defer.inlineCallbacks |
493 | + def test__get_boot_sources_v1_sets_os_to_wildcard(self): |
494 | + sources = [ |
495 | + { |
496 | + 'path': factory.make_url(), |
497 | + 'selections': [ |
498 | + { |
499 | + 'release': "trusty", |
500 | + 'arches': ["amd64"], |
501 | + 'subarches': ["generic"], |
502 | + 'labels': ["release"], |
503 | + }, |
504 | + { |
505 | + 'release': "precise", |
506 | + 'arches': ["amd64"], |
507 | + 'subarches': ["generic"], |
508 | + 'labels': ["release"], |
509 | + }, |
510 | + ], |
511 | + }, |
512 | + ] |
513 | + |
514 | + clock = Clock() |
515 | + client_call = Mock() |
516 | + client_call.side_effect = [ |
517 | + defer.fail(NoSuchMethod()), |
518 | + defer.succeed(dict(sources=sources)), |
519 | + ] |
520 | + |
521 | + service = PeriodicImageDownloadService( |
522 | + sentinel.rpc, clock, sentinel.uuid) |
523 | + sources = yield service._get_boot_sources(client_call) |
524 | + os_selections = [ |
525 | + selection.get('os') |
526 | + for source in sources['sources'] |
527 | + for selection in source['selections'] |
528 | + ] |
529 | + self.assertEqual(['*', '*'], os_selections) |
530 | |
531 | === renamed file 'src/provisioningserver/tftp.py' => 'src/provisioningserver/pserv_services/tftp.py' |
532 | === modified file 'src/provisioningserver/rpc/boot_images.py' |
533 | --- src/provisioningserver/rpc/boot_images.py 2014-08-26 17:23:16 +0000 |
534 | +++ src/provisioningserver/rpc/boot_images.py 2014-08-27 06:01:25 +0000 |
535 | @@ -15,39 +15,17 @@ |
536 | __all__ = [ |
537 | "import_boot_images", |
538 | "list_boot_images", |
539 | - "PeriodicImageDownloadService", |
540 | ] |
541 | |
542 | |
543 | -from datetime import timedelta |
544 | - |
545 | from provisioningserver.auth import MAAS_USER_GPGHOME |
546 | from provisioningserver.boot import tftppath |
547 | from provisioningserver.config import Config |
548 | from provisioningserver.import_images import boot_resources |
549 | -from provisioningserver.logger import get_maas_logger |
550 | -from provisioningserver.rpc.exceptions import NoConnectionsAvailable |
551 | -from provisioningserver.rpc.region import ( |
552 | - GetBootSources, |
553 | - GetBootSourcesV2, |
554 | - ) |
555 | from provisioningserver.utils.env import environment_variables |
556 | -from provisioningserver.utils.twisted import ( |
557 | - pause, |
558 | - synchronous, |
559 | - ) |
560 | -from twisted.application.internet import TimerService |
561 | -from twisted.internet.defer import ( |
562 | - DeferredLock, |
563 | - inlineCallbacks, |
564 | - returnValue, |
565 | - ) |
566 | +from provisioningserver.utils.twisted import synchronous |
567 | +from twisted.internet.defer import inlineCallbacks |
568 | from twisted.internet.threads import deferToThread |
569 | -from twisted.spread.pb import NoSuchMethod |
570 | - |
571 | - |
572 | -maaslog = get_maas_logger("boot_images") |
573 | -service_lock = DeferredLock() |
574 | |
575 | |
576 | def list_boot_images(): |
577 | @@ -73,82 +51,3 @@ |
578 | def import_boot_images(sources): |
579 | """Imports the boot images from the given sources.""" |
580 | yield deferToThread(_run_import, sources) |
581 | - |
582 | - |
583 | -class PeriodicImageDownloadService(TimerService, object): |
584 | - """Twisted service to periodically refresh ephemeral images. |
585 | - |
586 | - :param client_service: A `ClusterClientService` instance for talking |
587 | - to the region controller. |
588 | - :param reactor: An `IReactor` instance. |
589 | - """ |
590 | - |
591 | - check_interval = timedelta(minutes=5).total_seconds() |
592 | - |
593 | - def __init__(self, client_service, reactor, cluster_uuid): |
594 | - # Call self.check() every self.check_interval. |
595 | - super(PeriodicImageDownloadService, self).__init__( |
596 | - self.check_interval, self.maybe_start_download) |
597 | - self.clock = reactor |
598 | - self.client_service = client_service |
599 | - self.uuid = cluster_uuid |
600 | - |
601 | - @inlineCallbacks |
602 | - def _get_boot_sources(self, client): |
603 | - """Gets the boot sources from the region.""" |
604 | - try: |
605 | - sources = yield client(GetBootSourcesV2, uuid=self.uuid) |
606 | - except NoSuchMethod: |
607 | - # Region has not been upgraded to support the new call, use the |
608 | - # old call. The old call did not provide the new os selection |
609 | - # parameter. Region does not support boot source selection by os, |
610 | - # so its set too allow all operating systems. |
611 | - sources = yield client(GetBootSources, uuid=self.uuid) |
612 | - for source in sources['sources']: |
613 | - for selection in source['selections']: |
614 | - selection['os'] = '*' |
615 | - returnValue(sources) |
616 | - |
617 | - @inlineCallbacks |
618 | - def _start_download(self): |
619 | - client = None |
620 | - # Retry a few times, since this service usually comes up before |
621 | - # the RPC service. |
622 | - for _ in range(3): |
623 | - try: |
624 | - client = self.client_service.getClient() |
625 | - break |
626 | - except NoConnectionsAvailable: |
627 | - yield pause(5) |
628 | - if client is None: |
629 | - maaslog.error( |
630 | - "Can't initiate image download, no RPC connection to region.") |
631 | - return |
632 | - |
633 | - # Get sources from region |
634 | - sources = yield self._get_boot_sources(client) |
635 | - yield import_boot_images(sources.get("sources")) |
636 | - |
637 | - @inlineCallbacks |
638 | - def maybe_start_download(self): |
639 | - """Check the time the last image refresh happened and initiate a new |
640 | - one if older than 15 minutes. |
641 | - """ |
642 | - # Use a DeferredLock to prevent simultaneous downloads. |
643 | - if service_lock.locked: |
644 | - # Don't want to block on lock release. |
645 | - return |
646 | - yield service_lock.acquire() |
647 | - try: |
648 | - last_modified = tftppath.maas_meta_last_modified() |
649 | - if last_modified is None: |
650 | - # Don't auto-refresh if the user has never manually initiated |
651 | - # a download. |
652 | - return |
653 | - |
654 | - age_in_seconds = self.clock.seconds() - last_modified |
655 | - if age_in_seconds >= timedelta(minutes=15).total_seconds(): |
656 | - yield self._start_download() |
657 | - |
658 | - finally: |
659 | - service_lock.release() |
660 | |
661 | === modified file 'src/provisioningserver/rpc/power.py' |
662 | --- src/provisioningserver/rpc/power.py 2014-08-22 16:49:23 +0000 |
663 | +++ src/provisioningserver/rpc/power.py 2014-08-27 06:01:25 +0000 |
664 | @@ -13,7 +13,8 @@ |
665 | |
666 | __metaclass__ = type |
667 | __all__ = [ |
668 | - "get_power_state" |
669 | + "get_power_state", |
670 | + "query_all_nodes", |
671 | ] |
672 | |
673 | |
674 | @@ -27,17 +28,12 @@ |
675 | PowerActionFail, |
676 | ) |
677 | from provisioningserver.rpc import getRegionClient |
678 | -from provisioningserver.rpc.exceptions import ( |
679 | - NoConnectionsAvailable, |
680 | - NoSuchNode, |
681 | - ) |
682 | +from provisioningserver.rpc.exceptions import NoSuchNode |
683 | from provisioningserver.rpc.region import ( |
684 | - ListNodePowerParameters, |
685 | MarkNodeBroken, |
686 | UpdateNodePowerState, |
687 | ) |
688 | from provisioningserver.utils.twisted import pause |
689 | -from twisted.application.internet import TimerService |
690 | from twisted.internet import reactor |
691 | from twisted.internet.defer import ( |
692 | inlineCallbacks, |
693 | @@ -256,45 +252,3 @@ |
694 | maaslog.debug( |
695 | "%s: Could not update power status; " |
696 | "no such node.", hostname) |
697 | - |
698 | - |
699 | -class NodePowerMonitorService(TimerService, object): |
700 | - """Twisted service to monitor the status of all nodes |
701 | - controlled by this cluster. |
702 | - |
703 | - :param client_service: A `ClusterClientService` instance for talking |
704 | - to the region controller. |
705 | - :param reactor: An `IReactor` instance. |
706 | - """ |
707 | - |
708 | - check_interval = 600 # 5 minutes. |
709 | - |
710 | - def __init__(self, client_service, reactor, cluster_uuid): |
711 | - # Call self.check() every self.check_interval. |
712 | - super(NodePowerMonitorService, self).__init__( |
713 | - self.check_interval, self.query_nodes) |
714 | - self.clock = reactor |
715 | - self.client_service = client_service |
716 | - self.uuid = cluster_uuid |
717 | - |
718 | - @inlineCallbacks |
719 | - def query_nodes(self): |
720 | - client = None |
721 | - # Retry a few times, since this service usually comes up before |
722 | - # the RPC service. |
723 | - for _ in range(3): |
724 | - try: |
725 | - client = self.client_service.getClient() |
726 | - break |
727 | - except NoConnectionsAvailable: |
728 | - yield pause(5) |
729 | - if client is None: |
730 | - maaslog.error( |
731 | - "Can't query nodes's BMC for power state, no RPC connection " |
732 | - "to region.") |
733 | - return |
734 | - |
735 | - # Get the nodes from the Region |
736 | - response = yield client(ListNodePowerParameters, uuid=self.uuid) |
737 | - nodes = response['nodes'] |
738 | - yield query_all_nodes(nodes) |
739 | |
740 | === modified file 'src/provisioningserver/rpc/tests/test_boot_images.py' |
741 | --- src/provisioningserver/rpc/tests/test_boot_images.py 2014-08-26 17:23:16 +0000 |
742 | +++ src/provisioningserver/rpc/tests/test_boot_images.py 2014-08-27 06:01:25 +0000 |
743 | @@ -14,21 +14,11 @@ |
744 | __metaclass__ = type |
745 | __all__ = [] |
746 | |
747 | -from datetime import timedelta |
748 | import os |
749 | |
750 | from maastesting.factory import factory |
751 | -from maastesting.matchers import ( |
752 | - get_mock_calls, |
753 | - MockCalledOnceWith, |
754 | - MockCallsMatch, |
755 | - MockNotCalled, |
756 | - ) |
757 | -from mock import ( |
758 | - call, |
759 | - Mock, |
760 | - sentinel, |
761 | - ) |
762 | +from maastesting.matchers import MockCalledOnceWith |
763 | +from mock import sentinel |
764 | from provisioningserver.boot import tftppath |
765 | from provisioningserver.config import Config |
766 | from provisioningserver.import_images import boot_resources |
767 | @@ -37,22 +27,11 @@ |
768 | _run_import, |
769 | import_boot_images, |
770 | list_boot_images, |
771 | - PeriodicImageDownloadService, |
772 | - service_lock, |
773 | - ) |
774 | -from provisioningserver.rpc.exceptions import NoConnectionsAvailable |
775 | -from provisioningserver.rpc.region import ( |
776 | - GetBootSources, |
777 | - GetBootSourcesV2, |
778 | ) |
779 | from provisioningserver.testing.config import BootSourcesFixture |
780 | from provisioningserver.testing.testcase import PservTestCase |
781 | -from provisioningserver.utils.twisted import pause |
782 | from testtools.deferredruntest import AsynchronousDeferredRunTest |
783 | -from twisted.application.internet import TimerService |
784 | from twisted.internet import defer |
785 | -from twisted.internet.task import Clock |
786 | -from twisted.spread.pb import NoSuchMethod |
787 | |
788 | |
789 | class TestListBootImages(PservTestCase): |
790 | @@ -145,232 +124,3 @@ |
791 | self.assertThat( |
792 | deferToThread, MockCalledOnceWith( |
793 | _run_import, sentinel.sources)) |
794 | - |
795 | - |
796 | -class TestPeriodicImageDownloadService(PservTestCase): |
797 | - |
798 | - run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5) |
799 | - |
800 | - def test_init(self): |
801 | - service = PeriodicImageDownloadService( |
802 | - sentinel.service, sentinel.clock, sentinel.uuid) |
803 | - self.assertIsInstance(service, TimerService) |
804 | - self.assertIs(service.clock, sentinel.clock) |
805 | - self.assertIs(service.uuid, sentinel.uuid) |
806 | - self.assertIs(service.client_service, sentinel.service) |
807 | - |
808 | - def patch_download(self, service, return_value): |
809 | - patched = self.patch(service, '_start_download') |
810 | - patched.return_value = defer.succeed(return_value) |
811 | - return patched |
812 | - |
813 | - def test_is_called_every_interval(self): |
814 | - clock = Clock() |
815 | - service = PeriodicImageDownloadService( |
816 | - sentinel.service, clock, sentinel.uuid) |
817 | - # Avoid actual downloads: |
818 | - self.patch_download(service, None) |
819 | - maas_meta_last_modified = self.patch( |
820 | - tftppath, 'maas_meta_last_modified') |
821 | - maas_meta_last_modified.return_value = None |
822 | - service.startService() |
823 | - |
824 | - # The first call is issued at startup. |
825 | - self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified))) |
826 | - |
827 | - # Wind clock forward one second less than the desired interval. |
828 | - clock.advance(service.check_interval - 1) |
829 | - # No more periodic calls made. |
830 | - self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified))) |
831 | - |
832 | - # Wind clock forward one second, past the interval. |
833 | - clock.advance(1) |
834 | - |
835 | - # Now there were two calls. |
836 | - self.assertEqual(2, len(get_mock_calls(maas_meta_last_modified))) |
837 | - |
838 | - # Forward another interval, should be three calls. |
839 | - clock.advance(service.check_interval) |
840 | - self.assertEqual(3, len(get_mock_calls(maas_meta_last_modified))) |
841 | - |
842 | - def test_no_download_if_no_meta_file(self): |
843 | - clock = Clock() |
844 | - service = PeriodicImageDownloadService( |
845 | - sentinel.service, clock, sentinel.uuid) |
846 | - _start_download = self.patch_download(service, None) |
847 | - self.patch( |
848 | - tftppath, |
849 | - 'maas_meta_last_modified').return_value = None |
850 | - service.startService() |
851 | - self.assertThat(_start_download, MockNotCalled()) |
852 | - |
853 | - def test_initiates_download_if_15_minutes_has_passed(self): |
854 | - clock = Clock() |
855 | - service = PeriodicImageDownloadService( |
856 | - sentinel.service, clock, sentinel.uuid) |
857 | - _start_download = self.patch_download(service, None) |
858 | - one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds() |
859 | - self.patch( |
860 | - tftppath, |
861 | - 'maas_meta_last_modified').return_value = one_week_ago |
862 | - service.startService() |
863 | - self.assertThat(_start_download, MockCalledOnceWith()) |
864 | - |
865 | - def test_no_download_if_15_minutes_has_not_passed(self): |
866 | - clock = Clock() |
867 | - service = PeriodicImageDownloadService( |
868 | - sentinel.service, clock, sentinel.uuid) |
869 | - _start_download = self.patch_download(service, None) |
870 | - one_week = timedelta(minutes=15).total_seconds() |
871 | - self.patch( |
872 | - tftppath, |
873 | - 'maas_meta_last_modified').return_value = clock.seconds() |
874 | - clock.advance(one_week - 1) |
875 | - service.startService() |
876 | - self.assertThat(_start_download, MockNotCalled()) |
877 | - |
878 | - def test_download_is_initiated_in_new_thread(self): |
879 | - clock = Clock() |
880 | - maas_meta_last_modified = self.patch( |
881 | - tftppath, 'maas_meta_last_modified') |
882 | - one_week = timedelta(minutes=15).total_seconds() |
883 | - maas_meta_last_modified.return_value = clock.seconds() - one_week |
884 | - rpc_client = Mock() |
885 | - client_call = Mock() |
886 | - client_call.side_effect = [ |
887 | - defer.succeed(dict(sources=sentinel.sources)), |
888 | - ] |
889 | - rpc_client.getClient.return_value = client_call |
890 | - |
891 | - # We could patch out 'import_boot_images' instead here but I |
892 | - # don't do that for 2 reasons: |
893 | - # 1. It requires spinning the reactor again before being able to |
894 | - # test the result. |
895 | - # 2. It means there's no thread to clean up after the test. |
896 | - deferToThread = self.patch(boot_images, 'deferToThread') |
897 | - deferToThread.return_value = defer.succeed(None) |
898 | - service = PeriodicImageDownloadService( |
899 | - rpc_client, clock, sentinel.uuid) |
900 | - service.startService() |
901 | - self.assertThat( |
902 | - deferToThread, MockCalledOnceWith( |
903 | - _run_import, sentinel.sources)) |
904 | - |
905 | - def test_no_download_if_no_rpc_connections(self): |
906 | - rpc_client = Mock() |
907 | - failure = NoConnectionsAvailable() |
908 | - rpc_client.getClient.return_value.side_effect = failure |
909 | - |
910 | - deferToThread = self.patch(boot_images, 'deferToThread') |
911 | - service = PeriodicImageDownloadService( |
912 | - rpc_client, Clock(), sentinel.uuid) |
913 | - service.startService() |
914 | - self.assertThat(deferToThread, MockNotCalled()) |
915 | - |
916 | - @defer.inlineCallbacks |
917 | - def test_does_not_run_if_lock_taken(self): |
918 | - maas_meta_last_modified = self.patch( |
919 | - tftppath, 'maas_meta_last_modified') |
920 | - yield service_lock.acquire() |
921 | - self.addCleanup(service_lock.release) |
922 | - service = PeriodicImageDownloadService( |
923 | - sentinel.rpc, Clock(), sentinel.uuid) |
924 | - service.startService() |
925 | - self.assertThat(maas_meta_last_modified, MockNotCalled()) |
926 | - |
927 | - def test_takes_lock_when_running(self): |
928 | - clock = Clock() |
929 | - service = PeriodicImageDownloadService( |
930 | - sentinel.rpc, clock, sentinel.uuid) |
931 | - |
932 | - # Patch the download func so it's just a Deferred that waits for |
933 | - # one second. |
934 | - _start_download = self.patch(service, '_start_download') |
935 | - _start_download.return_value = pause(1, clock) |
936 | - |
937 | - # Set conditions for a required download: |
938 | - one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds() |
939 | - self.patch( |
940 | - tftppath, |
941 | - 'maas_meta_last_modified').return_value = one_week_ago |
942 | - |
943 | - # Lock is acquired for the first download after startup. |
944 | - service.startService() |
945 | - self.assertTrue(service_lock.locked) |
946 | - |
947 | - # Lock is released once the download is done. |
948 | - clock.advance(1) |
949 | - self.assertFalse(service_lock.locked) |
950 | - |
951 | - @defer.inlineCallbacks |
952 | - def test__get_boot_sources_calls_get_boot_sources_v2_before_v1(self): |
953 | - clock = Clock() |
954 | - client_call = Mock() |
955 | - client_call.side_effect = [ |
956 | - defer.succeed(dict(sources=sentinel.sources)), |
957 | - ] |
958 | - |
959 | - service = PeriodicImageDownloadService( |
960 | - sentinel.rpc, clock, sentinel.uuid) |
961 | - sources = yield service._get_boot_sources(client_call) |
962 | - self.assertEqual(sources.get('sources'), sentinel.sources) |
963 | - self.assertThat( |
964 | - client_call, |
965 | - MockCalledOnceWith(GetBootSourcesV2, uuid=sentinel.uuid)) |
966 | - |
967 | - @defer.inlineCallbacks |
968 | - def test__get_boot_sources_calls_get_boot_sources_v1_on_v2_missing(self): |
969 | - clock = Clock() |
970 | - client_call = Mock() |
971 | - client_call.side_effect = [ |
972 | - defer.fail(NoSuchMethod()), |
973 | - defer.succeed(dict(sources=[])), |
974 | - ] |
975 | - |
976 | - service = PeriodicImageDownloadService( |
977 | - sentinel.rpc, clock, sentinel.uuid) |
978 | - yield service._get_boot_sources(client_call) |
979 | - self.assertThat( |
980 | - client_call, |
981 | - MockCallsMatch( |
982 | - call(GetBootSourcesV2, uuid=sentinel.uuid), |
983 | - call(GetBootSources, uuid=sentinel.uuid))) |
984 | - |
985 | - @defer.inlineCallbacks |
986 | - def test__get_boot_sources_v1_sets_os_to_wildcard(self): |
987 | - sources = [ |
988 | - { |
989 | - 'path': factory.make_url(), |
990 | - 'selections': [ |
991 | - { |
992 | - 'release': "trusty", |
993 | - 'arches': ["amd64"], |
994 | - 'subarches': ["generic"], |
995 | - 'labels': ["release"], |
996 | - }, |
997 | - { |
998 | - 'release': "precise", |
999 | - 'arches': ["amd64"], |
1000 | - 'subarches': ["generic"], |
1001 | - 'labels': ["release"], |
1002 | - }, |
1003 | - ], |
1004 | - }, |
1005 | - ] |
1006 | - |
1007 | - clock = Clock() |
1008 | - client_call = Mock() |
1009 | - client_call.side_effect = [ |
1010 | - defer.fail(NoSuchMethod()), |
1011 | - defer.succeed(dict(sources=sources)), |
1012 | - ] |
1013 | - |
1014 | - service = PeriodicImageDownloadService( |
1015 | - sentinel.rpc, clock, sentinel.uuid) |
1016 | - sources = yield service._get_boot_sources(client_call) |
1017 | - os_selections = [ |
1018 | - selection.get('os') |
1019 | - for source in sources['sources'] |
1020 | - for selection in source['selections'] |
1021 | - ] |
1022 | - self.assertEqual(['*', '*'], os_selections) |
1023 | |
1024 | === modified file 'src/provisioningserver/tests/test_plugin.py' |
1025 | --- src/provisioningserver/tests/test_plugin.py 2014-08-26 23:55:04 +0000 |
1026 | +++ src/provisioningserver/tests/test_plugin.py 2014-08-27 06:01:25 +0000 |
1027 | @@ -27,9 +27,13 @@ |
1028 | ProvisioningServiceMaker, |
1029 | SingleUsernamePasswordChecker, |
1030 | ) |
1031 | -from provisioningserver.rpc.boot_images import PeriodicImageDownloadService |
1032 | -from provisioningserver.rpc.power import NodePowerMonitorService |
1033 | -from provisioningserver.tftp import ( |
1034 | +from provisioningserver.pserv_services.image_download_service import ( |
1035 | + PeriodicImageDownloadService, |
1036 | + ) |
1037 | +from provisioningserver.pserv_services.node_power_monitor_service import ( |
1038 | + NodePowerMonitorService, |
1039 | + ) |
1040 | +from provisioningserver.pserv_services.tftp import ( |
1041 | TFTPBackend, |
1042 | TFTPService, |
1043 | ) |
1044 | |
1045 | === modified file 'src/provisioningserver/tests/test_tftp.py' |
1046 | --- src/provisioningserver/tests/test_tftp.py 2014-08-13 21:49:35 +0000 |
1047 | +++ src/provisioningserver/tests/test_tftp.py 2014-08-27 06:01:25 +0000 |
1048 | @@ -37,17 +37,17 @@ |
1049 | IPV4_LINK_LOCAL, |
1050 | IPV6_LINK_LOCAL, |
1051 | ) |
1052 | -from provisioningserver import tftp as tftp_module |
1053 | from provisioningserver.boot import BytesReader |
1054 | from provisioningserver.boot.pxe import PXEBootMethod |
1055 | from provisioningserver.boot.tests.test_pxe import compose_config_path |
1056 | -from provisioningserver.tests.test_kernel_opts import make_kernel_parameters |
1057 | -from provisioningserver.tftp import ( |
1058 | +from provisioningserver.pserv_services import tftp as tftp_module |
1059 | +from provisioningserver.pserv_services.tftp import ( |
1060 | Port, |
1061 | TFTPBackend, |
1062 | TFTPService, |
1063 | UDPServer, |
1064 | ) |
1065 | +from provisioningserver.tests.test_kernel_opts import make_kernel_parameters |
1066 | from testtools.deferredruntest import AsynchronousDeferredRunTest |
1067 | from testtools.matchers import ( |
1068 | AfterPreprocessing, |
I promise I have not hidden any sneaky code changes inside the moved code ;)