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
=== modified file 'src/provisioningserver/boot/tests/test_powernv.py'
--- src/provisioningserver/boot/tests/test_powernv.py 2014-08-13 21:49:35 +0000
+++ src/provisioningserver/boot/tests/test_powernv.py 2014-08-27 06:01:25 +0000
@@ -31,9 +31,9 @@
31 )31 )
32from provisioningserver.boot.tests.test_pxe import parse_pxe_config32from provisioningserver.boot.tests.test_pxe import parse_pxe_config
33from provisioningserver.boot.tftppath import compose_image_path33from provisioningserver.boot.tftppath import compose_image_path
34from provisioningserver.pserv_services.tftp import TFTPBackend
34from provisioningserver.testing.config import set_tftp_root35from provisioningserver.testing.config import set_tftp_root
35from provisioningserver.tests.test_kernel_opts import make_kernel_parameters36from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
36from provisioningserver.tftp import TFTPBackend
37from testtools.matchers import (37from testtools.matchers import (
38 IsInstance,38 IsInstance,
39 MatchesAll,39 MatchesAll,
4040
=== modified file 'src/provisioningserver/plugin.py'
--- src/provisioningserver/plugin.py 2014-08-26 23:55:04 +0000
+++ src/provisioningserver/plugin.py 2014-08-27 06:01:25 +0000
@@ -31,10 +31,14 @@
31import provisioningserver31import provisioningserver
32from provisioningserver.cluster_config import get_cluster_uuid32from provisioningserver.cluster_config import get_cluster_uuid
33from provisioningserver.config import Config33from provisioningserver.config import Config
34from provisioningserver.rpc.boot_images import PeriodicImageDownloadService34from provisioningserver.pserv_services.image_download_service import (
35 PeriodicImageDownloadService,
36 )
37from provisioningserver.pserv_services.node_power_monitor_service import (
38 NodePowerMonitorService,
39 )
40from provisioningserver.pserv_services.tftp import TFTPService
35from provisioningserver.rpc.clusterservice import ClusterClientService41from provisioningserver.rpc.clusterservice import ClusterClientService
36from provisioningserver.rpc.power import NodePowerMonitorService
37from provisioningserver.tftp import TFTPService
38from twisted.application.internet import TCPServer42from twisted.application.internet import TCPServer
39from twisted.application.service import (43from twisted.application.service import (
40 IServiceMaker,44 IServiceMaker,
4145
=== added directory 'src/provisioningserver/pserv_services'
=== added file 'src/provisioningserver/pserv_services/__init__.py'
=== added file 'src/provisioningserver/pserv_services/image_download_service.py'
--- src/provisioningserver/pserv_services/image_download_service.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/pserv_services/image_download_service.py 2014-08-27 06:01:25 +0000
@@ -0,0 +1,120 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Service to periodically refresh the boot images."""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = [
16 "PeriodicImageDownloadService",
17 ]
18
19
20from datetime import timedelta
21
22from provisioningserver.boot import tftppath
23from provisioningserver.logger import get_maas_logger
24from provisioningserver.rpc.boot_images import import_boot_images
25from provisioningserver.rpc.exceptions import NoConnectionsAvailable
26from provisioningserver.rpc.region import (
27 GetBootSources,
28 GetBootSourcesV2,
29 )
30from provisioningserver.utils.twisted import pause
31from twisted.application.internet import TimerService
32from twisted.internet.defer import (
33 DeferredLock,
34 inlineCallbacks,
35 returnValue,
36 )
37from twisted.spread.pb import NoSuchMethod
38
39
40maaslog = get_maas_logger("boot_image_download_service")
41service_lock = DeferredLock()
42
43
44class PeriodicImageDownloadService(TimerService, object):
45 """Twisted service to periodically refresh ephemeral images.
46
47 :param client_service: A `ClusterClientService` instance for talking
48 to the region controller.
49 :param reactor: An `IReactor` instance.
50 """
51
52 check_interval = timedelta(minutes=5).total_seconds()
53
54 def __init__(self, client_service, reactor, cluster_uuid):
55 # Call self.check() every self.check_interval.
56 super(PeriodicImageDownloadService, self).__init__(
57 self.check_interval, self.maybe_start_download)
58 self.clock = reactor
59 self.client_service = client_service
60 self.uuid = cluster_uuid
61
62 @inlineCallbacks
63 def _get_boot_sources(self, client):
64 """Gets the boot sources from the region."""
65 try:
66 sources = yield client(GetBootSourcesV2, uuid=self.uuid)
67 except NoSuchMethod:
68 # Region has not been upgraded to support the new call, use the
69 # old call. The old call did not provide the new os selection
70 # parameter. Region does not support boot source selection by os,
71 # so its set too allow all operating systems.
72 sources = yield client(GetBootSources, uuid=self.uuid)
73 for source in sources['sources']:
74 for selection in source['selections']:
75 selection['os'] = '*'
76 returnValue(sources)
77
78 @inlineCallbacks
79 def _start_download(self):
80 client = None
81 # Retry a few times, since this service usually comes up before
82 # the RPC service.
83 for _ in range(3):
84 try:
85 client = self.client_service.getClient()
86 break
87 except NoConnectionsAvailable:
88 yield pause(5)
89 if client is None:
90 maaslog.error(
91 "Can't initiate image download, no RPC connection to region.")
92 return
93
94 # Get sources from region
95 sources = yield self._get_boot_sources(client)
96 yield import_boot_images(sources.get("sources"))
97
98 @inlineCallbacks
99 def maybe_start_download(self):
100 """Check the time the last image refresh happened and initiate a new
101 one if older than 15 minutes.
102 """
103 # Use a DeferredLock to prevent simultaneous downloads.
104 if service_lock.locked:
105 # Don't want to block on lock release.
106 return
107 yield service_lock.acquire()
108 try:
109 last_modified = tftppath.maas_meta_last_modified()
110 if last_modified is None:
111 # Don't auto-refresh if the user has never manually initiated
112 # a download.
113 return
114
115 age_in_seconds = self.clock.seconds() - last_modified
116 if age_in_seconds >= timedelta(minutes=15).total_seconds():
117 yield self._start_download()
118
119 finally:
120 service_lock.release()
0121
=== added file 'src/provisioningserver/pserv_services/node_power_monitor_service.py'
--- src/provisioningserver/pserv_services/node_power_monitor_service.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/pserv_services/node_power_monitor_service.py 2014-08-27 06:01:25 +0000
@@ -0,0 +1,75 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Service to periodically query the power state on this cluster's nodes."""
5
6
7from __future__ import (
8 absolute_import,
9 print_function,
10 unicode_literals,
11 )
12
13str = None
14
15__metaclass__ = type
16__all__ = [
17 "NodePowerMonitorService"
18]
19
20
21from provisioningserver.logger.log import get_maas_logger
22from provisioningserver.rpc.exceptions import NoConnectionsAvailable
23from provisioningserver.rpc.power import query_all_nodes
24from provisioningserver.rpc.region import ListNodePowerParameters
25from provisioningserver.utils.twisted import pause
26from twisted.application.internet import TimerService
27from twisted.internet.defer import inlineCallbacks
28
29
30maaslog = get_maas_logger("power_monitor_service")
31
32
33class NodePowerMonitorService(TimerService, object):
34 """Twisted service to monitor the status of all nodes
35 controlled by this cluster.
36
37 :param client_service: A `ClusterClientService` instance for talking
38 to the region controller.
39 :param reactor: An `IReactor` instance.
40 """
41
42 # XXX 2014-08-27 bug=1361967
43 # This service is COMPLETELY UNTESTED.
44
45 check_interval = 600 # 5 minutes.
46
47 def __init__(self, client_service, reactor, cluster_uuid):
48 # Call self.check() every self.check_interval.
49 super(NodePowerMonitorService, self).__init__(
50 self.check_interval, self.query_nodes)
51 self.clock = reactor
52 self.client_service = client_service
53 self.uuid = cluster_uuid
54
55 @inlineCallbacks
56 def query_nodes(self):
57 client = None
58 # Retry a few times, since this service usually comes up before
59 # the RPC service.
60 for _ in range(3):
61 try:
62 client = self.client_service.getClient()
63 break
64 except NoConnectionsAvailable:
65 yield pause(5)
66 if client is None:
67 maaslog.error(
68 "Can't query nodes's BMC for power state, no RPC connection "
69 "to region.")
70 return
71
72 # Get the nodes from the Region
73 response = yield client(ListNodePowerParameters, uuid=self.uuid)
74 nodes = response['nodes']
75 yield query_all_nodes(nodes)
076
=== added directory 'src/provisioningserver/pserv_services/tests'
=== added file 'src/provisioningserver/pserv_services/tests/__init__.py'
=== added file 'src/provisioningserver/pserv_services/tests/test_image_download_service.py'
--- src/provisioningserver/pserv_services/tests/test_image_download_service.py 1970-01-01 00:00:00 +0000
+++ src/provisioningserver/pserv_services/tests/test_image_download_service.py 2014-08-27 06:01:25 +0000
@@ -0,0 +1,279 @@
1# Copyright 2014 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for provisioningserver.pserv_services.image_download_service"""
5
6from __future__ import (
7 absolute_import,
8 print_function,
9 unicode_literals,
10 )
11
12str = None
13
14__metaclass__ = type
15__all__ = []
16
17
18from datetime import timedelta
19
20from maastesting.factory import factory
21from maastesting.matchers import (
22 get_mock_calls,
23 MockCalledOnceWith,
24 MockCallsMatch,
25 MockNotCalled,
26 )
27from mock import (
28 call,
29 Mock,
30 sentinel,
31 )
32from provisioningserver.boot import tftppath
33from provisioningserver.pserv_services.image_download_service import (
34 PeriodicImageDownloadService,
35 service_lock,
36 )
37from provisioningserver.rpc import boot_images
38from provisioningserver.rpc.boot_images import _run_import
39from provisioningserver.rpc.exceptions import NoConnectionsAvailable
40from provisioningserver.rpc.region import (
41 GetBootSources,
42 GetBootSourcesV2,
43 )
44from provisioningserver.testing.testcase import PservTestCase
45from provisioningserver.utils.twisted import pause
46from testtools.deferredruntest import AsynchronousDeferredRunTest
47from twisted.application.internet import TimerService
48from twisted.internet import defer
49from twisted.internet.task import Clock
50from twisted.spread.pb import NoSuchMethod
51
52
53class TestPeriodicImageDownloadService(PservTestCase):
54
55 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
56
57 def test_init(self):
58 service = PeriodicImageDownloadService(
59 sentinel.service, sentinel.clock, sentinel.uuid)
60 self.assertIsInstance(service, TimerService)
61 self.assertIs(service.clock, sentinel.clock)
62 self.assertIs(service.uuid, sentinel.uuid)
63 self.assertIs(service.client_service, sentinel.service)
64
65 def patch_download(self, service, return_value):
66 patched = self.patch(service, '_start_download')
67 patched.return_value = defer.succeed(return_value)
68 return patched
69
70 def test_is_called_every_interval(self):
71 clock = Clock()
72 service = PeriodicImageDownloadService(
73 sentinel.service, clock, sentinel.uuid)
74 # Avoid actual downloads:
75 self.patch_download(service, None)
76 maas_meta_last_modified = self.patch(
77 tftppath, 'maas_meta_last_modified')
78 maas_meta_last_modified.return_value = None
79 service.startService()
80
81 # The first call is issued at startup.
82 self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified)))
83
84 # Wind clock forward one second less than the desired interval.
85 clock.advance(service.check_interval - 1)
86 # No more periodic calls made.
87 self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified)))
88
89 # Wind clock forward one second, past the interval.
90 clock.advance(1)
91
92 # Now there were two calls.
93 self.assertEqual(2, len(get_mock_calls(maas_meta_last_modified)))
94
95 # Forward another interval, should be three calls.
96 clock.advance(service.check_interval)
97 self.assertEqual(3, len(get_mock_calls(maas_meta_last_modified)))
98
99 def test_no_download_if_no_meta_file(self):
100 clock = Clock()
101 service = PeriodicImageDownloadService(
102 sentinel.service, clock, sentinel.uuid)
103 _start_download = self.patch_download(service, None)
104 self.patch(
105 tftppath,
106 'maas_meta_last_modified').return_value = None
107 service.startService()
108 self.assertThat(_start_download, MockNotCalled())
109
110 def test_initiates_download_if_15_minutes_has_passed(self):
111 clock = Clock()
112 service = PeriodicImageDownloadService(
113 sentinel.service, clock, sentinel.uuid)
114 _start_download = self.patch_download(service, None)
115 one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds()
116 self.patch(
117 tftppath,
118 'maas_meta_last_modified').return_value = one_week_ago
119 service.startService()
120 self.assertThat(_start_download, MockCalledOnceWith())
121
122 def test_no_download_if_15_minutes_has_not_passed(self):
123 clock = Clock()
124 service = PeriodicImageDownloadService(
125 sentinel.service, clock, sentinel.uuid)
126 _start_download = self.patch_download(service, None)
127 one_week = timedelta(minutes=15).total_seconds()
128 self.patch(
129 tftppath,
130 'maas_meta_last_modified').return_value = clock.seconds()
131 clock.advance(one_week - 1)
132 service.startService()
133 self.assertThat(_start_download, MockNotCalled())
134
135 def test_download_is_initiated_in_new_thread(self):
136 clock = Clock()
137 maas_meta_last_modified = self.patch(
138 tftppath, 'maas_meta_last_modified')
139 one_week = timedelta(minutes=15).total_seconds()
140 maas_meta_last_modified.return_value = clock.seconds() - one_week
141 rpc_client = Mock()
142 client_call = Mock()
143 client_call.side_effect = [
144 defer.succeed(dict(sources=sentinel.sources)),
145 ]
146 rpc_client.getClient.return_value = client_call
147
148 # We could patch out 'import_boot_images' instead here but I
149 # don't do that for 2 reasons:
150 # 1. It requires spinning the reactor again before being able to
151 # test the result.
152 # 2. It means there's no thread to clean up after the test.
153 deferToThread = self.patch(boot_images, 'deferToThread')
154 deferToThread.return_value = defer.succeed(None)
155 service = PeriodicImageDownloadService(
156 rpc_client, clock, sentinel.uuid)
157 service.startService()
158 self.assertThat(
159 deferToThread, MockCalledOnceWith(
160 _run_import, sentinel.sources))
161
162 def test_no_download_if_no_rpc_connections(self):
163 rpc_client = Mock()
164 failure = NoConnectionsAvailable()
165 rpc_client.getClient.return_value.side_effect = failure
166
167 deferToThread = self.patch(boot_images, 'deferToThread')
168 service = PeriodicImageDownloadService(
169 rpc_client, Clock(), sentinel.uuid)
170 service.startService()
171 self.assertThat(deferToThread, MockNotCalled())
172
173 @defer.inlineCallbacks
174 def test_does_not_run_if_lock_taken(self):
175 maas_meta_last_modified = self.patch(
176 tftppath, 'maas_meta_last_modified')
177 yield service_lock.acquire()
178 self.addCleanup(service_lock.release)
179 service = PeriodicImageDownloadService(
180 sentinel.rpc, Clock(), sentinel.uuid)
181 service.startService()
182 self.assertThat(maas_meta_last_modified, MockNotCalled())
183
184 def test_takes_lock_when_running(self):
185 clock = Clock()
186 service = PeriodicImageDownloadService(
187 sentinel.rpc, clock, sentinel.uuid)
188
189 # Patch the download func so it's just a Deferred that waits for
190 # one second.
191 _start_download = self.patch(service, '_start_download')
192 _start_download.return_value = pause(1, clock)
193
194 # Set conditions for a required download:
195 one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds()
196 self.patch(
197 tftppath,
198 'maas_meta_last_modified').return_value = one_week_ago
199
200 # Lock is acquired for the first download after startup.
201 service.startService()
202 self.assertTrue(service_lock.locked)
203
204 # Lock is released once the download is done.
205 clock.advance(1)
206 self.assertFalse(service_lock.locked)
207
208 @defer.inlineCallbacks
209 def test__get_boot_sources_calls_get_boot_sources_v2_before_v1(self):
210 clock = Clock()
211 client_call = Mock()
212 client_call.side_effect = [
213 defer.succeed(dict(sources=sentinel.sources)),
214 ]
215
216 service = PeriodicImageDownloadService(
217 sentinel.rpc, clock, sentinel.uuid)
218 sources = yield service._get_boot_sources(client_call)
219 self.assertEqual(sources.get('sources'), sentinel.sources)
220 self.assertThat(
221 client_call,
222 MockCalledOnceWith(GetBootSourcesV2, uuid=sentinel.uuid))
223
224 @defer.inlineCallbacks
225 def test__get_boot_sources_calls_get_boot_sources_v1_on_v2_missing(self):
226 clock = Clock()
227 client_call = Mock()
228 client_call.side_effect = [
229 defer.fail(NoSuchMethod()),
230 defer.succeed(dict(sources=[])),
231 ]
232
233 service = PeriodicImageDownloadService(
234 sentinel.rpc, clock, sentinel.uuid)
235 yield service._get_boot_sources(client_call)
236 self.assertThat(
237 client_call,
238 MockCallsMatch(
239 call(GetBootSourcesV2, uuid=sentinel.uuid),
240 call(GetBootSources, uuid=sentinel.uuid)))
241
242 @defer.inlineCallbacks
243 def test__get_boot_sources_v1_sets_os_to_wildcard(self):
244 sources = [
245 {
246 'path': factory.make_url(),
247 'selections': [
248 {
249 'release': "trusty",
250 'arches': ["amd64"],
251 'subarches': ["generic"],
252 'labels': ["release"],
253 },
254 {
255 'release': "precise",
256 'arches': ["amd64"],
257 'subarches': ["generic"],
258 'labels': ["release"],
259 },
260 ],
261 },
262 ]
263
264 clock = Clock()
265 client_call = Mock()
266 client_call.side_effect = [
267 defer.fail(NoSuchMethod()),
268 defer.succeed(dict(sources=sources)),
269 ]
270
271 service = PeriodicImageDownloadService(
272 sentinel.rpc, clock, sentinel.uuid)
273 sources = yield service._get_boot_sources(client_call)
274 os_selections = [
275 selection.get('os')
276 for source in sources['sources']
277 for selection in source['selections']
278 ]
279 self.assertEqual(['*', '*'], os_selections)
0280
=== renamed file 'src/provisioningserver/tftp.py' => 'src/provisioningserver/pserv_services/tftp.py'
=== modified file 'src/provisioningserver/rpc/boot_images.py'
--- src/provisioningserver/rpc/boot_images.py 2014-08-26 17:23:16 +0000
+++ src/provisioningserver/rpc/boot_images.py 2014-08-27 06:01:25 +0000
@@ -15,39 +15,17 @@
15__all__ = [15__all__ = [
16 "import_boot_images",16 "import_boot_images",
17 "list_boot_images",17 "list_boot_images",
18 "PeriodicImageDownloadService",
19 ]18 ]
2019
2120
22from datetime import timedelta
23
24from provisioningserver.auth import MAAS_USER_GPGHOME21from provisioningserver.auth import MAAS_USER_GPGHOME
25from provisioningserver.boot import tftppath22from provisioningserver.boot import tftppath
26from provisioningserver.config import Config23from provisioningserver.config import Config
27from provisioningserver.import_images import boot_resources24from provisioningserver.import_images import boot_resources
28from provisioningserver.logger import get_maas_logger
29from provisioningserver.rpc.exceptions import NoConnectionsAvailable
30from provisioningserver.rpc.region import (
31 GetBootSources,
32 GetBootSourcesV2,
33 )
34from provisioningserver.utils.env import environment_variables25from provisioningserver.utils.env import environment_variables
35from provisioningserver.utils.twisted import (26from provisioningserver.utils.twisted import synchronous
36 pause,27from twisted.internet.defer import inlineCallbacks
37 synchronous,
38 )
39from twisted.application.internet import TimerService
40from twisted.internet.defer import (
41 DeferredLock,
42 inlineCallbacks,
43 returnValue,
44 )
45from twisted.internet.threads import deferToThread28from twisted.internet.threads import deferToThread
46from twisted.spread.pb import NoSuchMethod
47
48
49maaslog = get_maas_logger("boot_images")
50service_lock = DeferredLock()
5129
5230
53def list_boot_images():31def list_boot_images():
@@ -73,82 +51,3 @@
73def import_boot_images(sources):51def import_boot_images(sources):
74 """Imports the boot images from the given sources."""52 """Imports the boot images from the given sources."""
75 yield deferToThread(_run_import, sources)53 yield deferToThread(_run_import, sources)
76
77
78class PeriodicImageDownloadService(TimerService, object):
79 """Twisted service to periodically refresh ephemeral images.
80
81 :param client_service: A `ClusterClientService` instance for talking
82 to the region controller.
83 :param reactor: An `IReactor` instance.
84 """
85
86 check_interval = timedelta(minutes=5).total_seconds()
87
88 def __init__(self, client_service, reactor, cluster_uuid):
89 # Call self.check() every self.check_interval.
90 super(PeriodicImageDownloadService, self).__init__(
91 self.check_interval, self.maybe_start_download)
92 self.clock = reactor
93 self.client_service = client_service
94 self.uuid = cluster_uuid
95
96 @inlineCallbacks
97 def _get_boot_sources(self, client):
98 """Gets the boot sources from the region."""
99 try:
100 sources = yield client(GetBootSourcesV2, uuid=self.uuid)
101 except NoSuchMethod:
102 # Region has not been upgraded to support the new call, use the
103 # old call. The old call did not provide the new os selection
104 # parameter. Region does not support boot source selection by os,
105 # so its set too allow all operating systems.
106 sources = yield client(GetBootSources, uuid=self.uuid)
107 for source in sources['sources']:
108 for selection in source['selections']:
109 selection['os'] = '*'
110 returnValue(sources)
111
112 @inlineCallbacks
113 def _start_download(self):
114 client = None
115 # Retry a few times, since this service usually comes up before
116 # the RPC service.
117 for _ in range(3):
118 try:
119 client = self.client_service.getClient()
120 break
121 except NoConnectionsAvailable:
122 yield pause(5)
123 if client is None:
124 maaslog.error(
125 "Can't initiate image download, no RPC connection to region.")
126 return
127
128 # Get sources from region
129 sources = yield self._get_boot_sources(client)
130 yield import_boot_images(sources.get("sources"))
131
132 @inlineCallbacks
133 def maybe_start_download(self):
134 """Check the time the last image refresh happened and initiate a new
135 one if older than 15 minutes.
136 """
137 # Use a DeferredLock to prevent simultaneous downloads.
138 if service_lock.locked:
139 # Don't want to block on lock release.
140 return
141 yield service_lock.acquire()
142 try:
143 last_modified = tftppath.maas_meta_last_modified()
144 if last_modified is None:
145 # Don't auto-refresh if the user has never manually initiated
146 # a download.
147 return
148
149 age_in_seconds = self.clock.seconds() - last_modified
150 if age_in_seconds >= timedelta(minutes=15).total_seconds():
151 yield self._start_download()
152
153 finally:
154 service_lock.release()
15554
=== modified file 'src/provisioningserver/rpc/power.py'
--- src/provisioningserver/rpc/power.py 2014-08-22 16:49:23 +0000
+++ src/provisioningserver/rpc/power.py 2014-08-27 06:01:25 +0000
@@ -13,7 +13,8 @@
1313
14__metaclass__ = type14__metaclass__ = type
15__all__ = [15__all__ = [
16 "get_power_state"16 "get_power_state",
17 "query_all_nodes",
17]18]
1819
1920
@@ -27,17 +28,12 @@
27 PowerActionFail,28 PowerActionFail,
28 )29 )
29from provisioningserver.rpc import getRegionClient30from provisioningserver.rpc import getRegionClient
30from provisioningserver.rpc.exceptions import (31from provisioningserver.rpc.exceptions import NoSuchNode
31 NoConnectionsAvailable,
32 NoSuchNode,
33 )
34from provisioningserver.rpc.region import (32from provisioningserver.rpc.region import (
35 ListNodePowerParameters,
36 MarkNodeBroken,33 MarkNodeBroken,
37 UpdateNodePowerState,34 UpdateNodePowerState,
38 )35 )
39from provisioningserver.utils.twisted import pause36from provisioningserver.utils.twisted import pause
40from twisted.application.internet import TimerService
41from twisted.internet import reactor37from twisted.internet import reactor
42from twisted.internet.defer import (38from twisted.internet.defer import (
43 inlineCallbacks,39 inlineCallbacks,
@@ -256,45 +252,3 @@
256 maaslog.debug(252 maaslog.debug(
257 "%s: Could not update power status; "253 "%s: Could not update power status; "
258 "no such node.", hostname)254 "no such node.", hostname)
259
260
261class NodePowerMonitorService(TimerService, object):
262 """Twisted service to monitor the status of all nodes
263 controlled by this cluster.
264
265 :param client_service: A `ClusterClientService` instance for talking
266 to the region controller.
267 :param reactor: An `IReactor` instance.
268 """
269
270 check_interval = 600 # 5 minutes.
271
272 def __init__(self, client_service, reactor, cluster_uuid):
273 # Call self.check() every self.check_interval.
274 super(NodePowerMonitorService, self).__init__(
275 self.check_interval, self.query_nodes)
276 self.clock = reactor
277 self.client_service = client_service
278 self.uuid = cluster_uuid
279
280 @inlineCallbacks
281 def query_nodes(self):
282 client = None
283 # Retry a few times, since this service usually comes up before
284 # the RPC service.
285 for _ in range(3):
286 try:
287 client = self.client_service.getClient()
288 break
289 except NoConnectionsAvailable:
290 yield pause(5)
291 if client is None:
292 maaslog.error(
293 "Can't query nodes's BMC for power state, no RPC connection "
294 "to region.")
295 return
296
297 # Get the nodes from the Region
298 response = yield client(ListNodePowerParameters, uuid=self.uuid)
299 nodes = response['nodes']
300 yield query_all_nodes(nodes)
301255
=== modified file 'src/provisioningserver/rpc/tests/test_boot_images.py'
--- src/provisioningserver/rpc/tests/test_boot_images.py 2014-08-26 17:23:16 +0000
+++ src/provisioningserver/rpc/tests/test_boot_images.py 2014-08-27 06:01:25 +0000
@@ -14,21 +14,11 @@
14__metaclass__ = type14__metaclass__ = type
15__all__ = []15__all__ = []
1616
17from datetime import timedelta
18import os17import os
1918
20from maastesting.factory import factory19from maastesting.factory import factory
21from maastesting.matchers import (20from maastesting.matchers import MockCalledOnceWith
22 get_mock_calls,21from mock import sentinel
23 MockCalledOnceWith,
24 MockCallsMatch,
25 MockNotCalled,
26 )
27from mock import (
28 call,
29 Mock,
30 sentinel,
31 )
32from provisioningserver.boot import tftppath22from provisioningserver.boot import tftppath
33from provisioningserver.config import Config23from provisioningserver.config import Config
34from provisioningserver.import_images import boot_resources24from provisioningserver.import_images import boot_resources
@@ -37,22 +27,11 @@
37 _run_import,27 _run_import,
38 import_boot_images,28 import_boot_images,
39 list_boot_images,29 list_boot_images,
40 PeriodicImageDownloadService,
41 service_lock,
42 )
43from provisioningserver.rpc.exceptions import NoConnectionsAvailable
44from provisioningserver.rpc.region import (
45 GetBootSources,
46 GetBootSourcesV2,
47 )30 )
48from provisioningserver.testing.config import BootSourcesFixture31from provisioningserver.testing.config import BootSourcesFixture
49from provisioningserver.testing.testcase import PservTestCase32from provisioningserver.testing.testcase import PservTestCase
50from provisioningserver.utils.twisted import pause
51from testtools.deferredruntest import AsynchronousDeferredRunTest33from testtools.deferredruntest import AsynchronousDeferredRunTest
52from twisted.application.internet import TimerService
53from twisted.internet import defer34from twisted.internet import defer
54from twisted.internet.task import Clock
55from twisted.spread.pb import NoSuchMethod
5635
5736
58class TestListBootImages(PservTestCase):37class TestListBootImages(PservTestCase):
@@ -145,232 +124,3 @@
145 self.assertThat(124 self.assertThat(
146 deferToThread, MockCalledOnceWith(125 deferToThread, MockCalledOnceWith(
147 _run_import, sentinel.sources))126 _run_import, sentinel.sources))
148
149
150class TestPeriodicImageDownloadService(PservTestCase):
151
152 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
153
154 def test_init(self):
155 service = PeriodicImageDownloadService(
156 sentinel.service, sentinel.clock, sentinel.uuid)
157 self.assertIsInstance(service, TimerService)
158 self.assertIs(service.clock, sentinel.clock)
159 self.assertIs(service.uuid, sentinel.uuid)
160 self.assertIs(service.client_service, sentinel.service)
161
162 def patch_download(self, service, return_value):
163 patched = self.patch(service, '_start_download')
164 patched.return_value = defer.succeed(return_value)
165 return patched
166
167 def test_is_called_every_interval(self):
168 clock = Clock()
169 service = PeriodicImageDownloadService(
170 sentinel.service, clock, sentinel.uuid)
171 # Avoid actual downloads:
172 self.patch_download(service, None)
173 maas_meta_last_modified = self.patch(
174 tftppath, 'maas_meta_last_modified')
175 maas_meta_last_modified.return_value = None
176 service.startService()
177
178 # The first call is issued at startup.
179 self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified)))
180
181 # Wind clock forward one second less than the desired interval.
182 clock.advance(service.check_interval - 1)
183 # No more periodic calls made.
184 self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified)))
185
186 # Wind clock forward one second, past the interval.
187 clock.advance(1)
188
189 # Now there were two calls.
190 self.assertEqual(2, len(get_mock_calls(maas_meta_last_modified)))
191
192 # Forward another interval, should be three calls.
193 clock.advance(service.check_interval)
194 self.assertEqual(3, len(get_mock_calls(maas_meta_last_modified)))
195
196 def test_no_download_if_no_meta_file(self):
197 clock = Clock()
198 service = PeriodicImageDownloadService(
199 sentinel.service, clock, sentinel.uuid)
200 _start_download = self.patch_download(service, None)
201 self.patch(
202 tftppath,
203 'maas_meta_last_modified').return_value = None
204 service.startService()
205 self.assertThat(_start_download, MockNotCalled())
206
207 def test_initiates_download_if_15_minutes_has_passed(self):
208 clock = Clock()
209 service = PeriodicImageDownloadService(
210 sentinel.service, clock, sentinel.uuid)
211 _start_download = self.patch_download(service, None)
212 one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds()
213 self.patch(
214 tftppath,
215 'maas_meta_last_modified').return_value = one_week_ago
216 service.startService()
217 self.assertThat(_start_download, MockCalledOnceWith())
218
219 def test_no_download_if_15_minutes_has_not_passed(self):
220 clock = Clock()
221 service = PeriodicImageDownloadService(
222 sentinel.service, clock, sentinel.uuid)
223 _start_download = self.patch_download(service, None)
224 one_week = timedelta(minutes=15).total_seconds()
225 self.patch(
226 tftppath,
227 'maas_meta_last_modified').return_value = clock.seconds()
228 clock.advance(one_week - 1)
229 service.startService()
230 self.assertThat(_start_download, MockNotCalled())
231
232 def test_download_is_initiated_in_new_thread(self):
233 clock = Clock()
234 maas_meta_last_modified = self.patch(
235 tftppath, 'maas_meta_last_modified')
236 one_week = timedelta(minutes=15).total_seconds()
237 maas_meta_last_modified.return_value = clock.seconds() - one_week
238 rpc_client = Mock()
239 client_call = Mock()
240 client_call.side_effect = [
241 defer.succeed(dict(sources=sentinel.sources)),
242 ]
243 rpc_client.getClient.return_value = client_call
244
245 # We could patch out 'import_boot_images' instead here but I
246 # don't do that for 2 reasons:
247 # 1. It requires spinning the reactor again before being able to
248 # test the result.
249 # 2. It means there's no thread to clean up after the test.
250 deferToThread = self.patch(boot_images, 'deferToThread')
251 deferToThread.return_value = defer.succeed(None)
252 service = PeriodicImageDownloadService(
253 rpc_client, clock, sentinel.uuid)
254 service.startService()
255 self.assertThat(
256 deferToThread, MockCalledOnceWith(
257 _run_import, sentinel.sources))
258
259 def test_no_download_if_no_rpc_connections(self):
260 rpc_client = Mock()
261 failure = NoConnectionsAvailable()
262 rpc_client.getClient.return_value.side_effect = failure
263
264 deferToThread = self.patch(boot_images, 'deferToThread')
265 service = PeriodicImageDownloadService(
266 rpc_client, Clock(), sentinel.uuid)
267 service.startService()
268 self.assertThat(deferToThread, MockNotCalled())
269
270 @defer.inlineCallbacks
271 def test_does_not_run_if_lock_taken(self):
272 maas_meta_last_modified = self.patch(
273 tftppath, 'maas_meta_last_modified')
274 yield service_lock.acquire()
275 self.addCleanup(service_lock.release)
276 service = PeriodicImageDownloadService(
277 sentinel.rpc, Clock(), sentinel.uuid)
278 service.startService()
279 self.assertThat(maas_meta_last_modified, MockNotCalled())
280
281 def test_takes_lock_when_running(self):
282 clock = Clock()
283 service = PeriodicImageDownloadService(
284 sentinel.rpc, clock, sentinel.uuid)
285
286 # Patch the download func so it's just a Deferred that waits for
287 # one second.
288 _start_download = self.patch(service, '_start_download')
289 _start_download.return_value = pause(1, clock)
290
291 # Set conditions for a required download:
292 one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds()
293 self.patch(
294 tftppath,
295 'maas_meta_last_modified').return_value = one_week_ago
296
297 # Lock is acquired for the first download after startup.
298 service.startService()
299 self.assertTrue(service_lock.locked)
300
301 # Lock is released once the download is done.
302 clock.advance(1)
303 self.assertFalse(service_lock.locked)
304
305 @defer.inlineCallbacks
306 def test__get_boot_sources_calls_get_boot_sources_v2_before_v1(self):
307 clock = Clock()
308 client_call = Mock()
309 client_call.side_effect = [
310 defer.succeed(dict(sources=sentinel.sources)),
311 ]
312
313 service = PeriodicImageDownloadService(
314 sentinel.rpc, clock, sentinel.uuid)
315 sources = yield service._get_boot_sources(client_call)
316 self.assertEqual(sources.get('sources'), sentinel.sources)
317 self.assertThat(
318 client_call,
319 MockCalledOnceWith(GetBootSourcesV2, uuid=sentinel.uuid))
320
321 @defer.inlineCallbacks
322 def test__get_boot_sources_calls_get_boot_sources_v1_on_v2_missing(self):
323 clock = Clock()
324 client_call = Mock()
325 client_call.side_effect = [
326 defer.fail(NoSuchMethod()),
327 defer.succeed(dict(sources=[])),
328 ]
329
330 service = PeriodicImageDownloadService(
331 sentinel.rpc, clock, sentinel.uuid)
332 yield service._get_boot_sources(client_call)
333 self.assertThat(
334 client_call,
335 MockCallsMatch(
336 call(GetBootSourcesV2, uuid=sentinel.uuid),
337 call(GetBootSources, uuid=sentinel.uuid)))
338
339 @defer.inlineCallbacks
340 def test__get_boot_sources_v1_sets_os_to_wildcard(self):
341 sources = [
342 {
343 'path': factory.make_url(),
344 'selections': [
345 {
346 'release': "trusty",
347 'arches': ["amd64"],
348 'subarches': ["generic"],
349 'labels': ["release"],
350 },
351 {
352 'release': "precise",
353 'arches': ["amd64"],
354 'subarches': ["generic"],
355 'labels': ["release"],
356 },
357 ],
358 },
359 ]
360
361 clock = Clock()
362 client_call = Mock()
363 client_call.side_effect = [
364 defer.fail(NoSuchMethod()),
365 defer.succeed(dict(sources=sources)),
366 ]
367
368 service = PeriodicImageDownloadService(
369 sentinel.rpc, clock, sentinel.uuid)
370 sources = yield service._get_boot_sources(client_call)
371 os_selections = [
372 selection.get('os')
373 for source in sources['sources']
374 for selection in source['selections']
375 ]
376 self.assertEqual(['*', '*'], os_selections)
377127
=== modified file 'src/provisioningserver/tests/test_plugin.py'
--- src/provisioningserver/tests/test_plugin.py 2014-08-26 23:55:04 +0000
+++ src/provisioningserver/tests/test_plugin.py 2014-08-27 06:01:25 +0000
@@ -27,9 +27,13 @@
27 ProvisioningServiceMaker,27 ProvisioningServiceMaker,
28 SingleUsernamePasswordChecker,28 SingleUsernamePasswordChecker,
29 )29 )
30from provisioningserver.rpc.boot_images import PeriodicImageDownloadService30from provisioningserver.pserv_services.image_download_service import (
31from provisioningserver.rpc.power import NodePowerMonitorService31 PeriodicImageDownloadService,
32from provisioningserver.tftp import (32 )
33from provisioningserver.pserv_services.node_power_monitor_service import (
34 NodePowerMonitorService,
35 )
36from provisioningserver.pserv_services.tftp import (
33 TFTPBackend,37 TFTPBackend,
34 TFTPService,38 TFTPService,
35 )39 )
3640
=== modified file 'src/provisioningserver/tests/test_tftp.py'
--- src/provisioningserver/tests/test_tftp.py 2014-08-13 21:49:35 +0000
+++ src/provisioningserver/tests/test_tftp.py 2014-08-27 06:01:25 +0000
@@ -37,17 +37,17 @@
37 IPV4_LINK_LOCAL,37 IPV4_LINK_LOCAL,
38 IPV6_LINK_LOCAL,38 IPV6_LINK_LOCAL,
39 )39 )
40from provisioningserver import tftp as tftp_module
41from provisioningserver.boot import BytesReader40from provisioningserver.boot import BytesReader
42from provisioningserver.boot.pxe import PXEBootMethod41from provisioningserver.boot.pxe import PXEBootMethod
43from provisioningserver.boot.tests.test_pxe import compose_config_path42from provisioningserver.boot.tests.test_pxe import compose_config_path
44from provisioningserver.tests.test_kernel_opts import make_kernel_parameters43from provisioningserver.pserv_services import tftp as tftp_module
45from provisioningserver.tftp import (44from provisioningserver.pserv_services.tftp import (
46 Port,45 Port,
47 TFTPBackend,46 TFTPBackend,
48 TFTPService,47 TFTPService,
49 UDPServer,48 UDPServer,
50 )49 )
50from provisioningserver.tests.test_kernel_opts import make_kernel_parameters
51from testtools.deferredruntest import AsynchronousDeferredRunTest51from testtools.deferredruntest import AsynchronousDeferredRunTest
52from testtools.matchers import (52from testtools.matchers import (
53 AfterPreprocessing,53 AfterPreprocessing,