Merge lp:~julian-edwards/maas/pserv-services-dir into lp:~maas-committers/maas/trunk

Proposed by Julian Edwards
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
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Review via email: mp+232344@code.launchpad.net

Commit message

Create a provisioningserver/pserv_services/ directory and put (nearly) all the twistd services in it.

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 provisioningserver.__init__.py defines a module property called "services" which clashes. I hate code in __init__.py!

There is one service left to migrate, ClusterClientService but I lost the will to live after slogging through the rest of the code, which was not just a simple bzr rename, but I had to split files because services had been mixed in with unrelated RPC code.

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

I promise I have not hidden any sneaky code changes inside the moved code ;)

Revision history for this message
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_monitor_service.py, could you file a bug and include its number in the comment?

.

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.

review: Approve
Revision history for this message
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_monitor_service.py, could you file a bug and
> 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.

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (20.5 KiB)

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://security.ubuntu.com trusty-security InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Get:1 http://security.ubuntu.com trusty-security Release.gpg [933 B]
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Get:2 http://security.ubuntu.com trusty-security Release [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Get:3 http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Get:4 http://nova.clouds.archive.ubuntu.com trusty-updates Release [59.7 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Get:5 http://security.ubuntu.com trusty-security/main Sources [41.3 kB]
Get:6 http://security.ubuntu.com trusty-security/universe Sources [11.3 kB]
Get:7 http://security.ubuntu.com trusty-security/main amd64 Packages [132 kB]
Get:8 http://security.ubuntu.com trusty-security/universe amd64 Packages [46.7 kB]
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Get:9 http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources [111 kB]
Get:10 http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources [78.1 kB]
Get:11 http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages [302 kB]
Get:12 http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages [188 kB]
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Fetched 1,032 kB in 0s (1,628 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi python...

Revision history for this message
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. :(

Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (18.8 KiB)

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://security.ubuntu.com trusty-security InRelease
Hit http://security.ubuntu.com trusty-security Release.gpg
Hit http://security.ubuntu.com trusty-security Release
Ign http://nova.clouds.archive.ubuntu.com trusty InRelease
Ign http://nova.clouds.archive.ubuntu.com trusty-updates InRelease
Hit http://nova.clouds.archive.ubuntu.com trusty Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release.gpg
Hit http://nova.clouds.archive.ubuntu.com trusty Release
Hit http://nova.clouds.archive.ubuntu.com trusty-updates Release
Hit http://security.ubuntu.com trusty-security/main Sources
Hit http://security.ubuntu.com trusty-security/universe Sources
Hit http://security.ubuntu.com trusty-security/main amd64 Packages
Hit http://security.ubuntu.com trusty-security/universe amd64 Packages
Hit http://security.ubuntu.com trusty-security/main Translation-en
Hit http://security.ubuntu.com trusty-security/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Sources
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe amd64 Packages
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/main Translation-en
Hit http://nova.clouds.archive.ubuntu.com trusty-updates/universe Translation-en
Ign http://nova.clouds.archive.ubuntu.com trusty/main Translation-en_US
Ign http://nova.clouds.archive.ubuntu.com trusty/universe Translation-en_US
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
     --no-install-recommends install apache2 authbind bind9 bind9utils build-essential bzr-builddeb curl daemontools debhelper dh-apport distro-info dnsutils firefox freeipmi-tools ipython isc-dhcp-common libjs-raphael libjs-yui3-full libjs-yui3-min libpq-dev make pep8 postgresql pyflakes python-amqplib python-bzrlib python-celery python-convoy python-crochet python-cssselect python-curtin python-dev python-distro-info python-django python-django-piston python-django-south python-djorm-ext-pgarray python-docutils python-extras python-fixtures python-flake8 python-formencode python-hivex python-httplib2 python-jinja2 python-jsonschema python-lockfile python-lxml python-mimeparse python-mock python-netaddr python-netifaces python-nose python-oauth python-oops python-oops-amqp python-oops-datedir-repo python-oops-twisted python-oops-wsgi python-openssl python-paramiko python-pexpect python-pip python-pocket-lint python-psycopg2 python-pyinotify python-seamicroclient python-simplejson python-simplestreams python-sphi...

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

Sigh.

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

Forgot to move the tftp test, re-landing.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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,