1
# Copyright 2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Tests for provisioningserver.pserv_services.image_download_service"""
6
from __future__ import (
18
from datetime import timedelta
20
from fixtures import FakeLogger
21
from maastesting.factory import factory
22
from maastesting.matchers import (
28
from maastesting.testcase import MAASTwistedRunTest
34
from provisioningserver.boot import tftppath
35
from provisioningserver.pserv_services.image_download_service import (
38
from provisioningserver.rpc import boot_images
39
from provisioningserver.rpc.boot_images import _run_import
40
from provisioningserver.rpc.exceptions import NoConnectionsAvailable
41
from provisioningserver.rpc.region import (
45
from provisioningserver.rpc.testing import TwistedLoggerFixture
46
from provisioningserver.testing.testcase import PservTestCase
47
from testtools.deferredruntest import extract_result
48
from twisted.application.internet import TimerService
49
from twisted.internet import defer
50
from twisted.internet.task import Clock
51
from twisted.spread.pb import NoSuchMethod
54
class TestPeriodicImageDownloadService(PservTestCase):
56
run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
59
service = ImageDownloadService(
60
sentinel.service, sentinel.clock, sentinel.uuid)
61
self.assertIsInstance(service, TimerService)
62
self.assertIs(service.clock, sentinel.clock)
63
self.assertIs(service.uuid, sentinel.uuid)
64
self.assertIs(service.client_service, sentinel.service)
66
def patch_download(self, service, return_value):
67
patched = self.patch(service, '_start_download')
68
patched.return_value = defer.succeed(return_value)
71
def test_is_called_every_interval(self):
73
service = ImageDownloadService(
74
sentinel.service, clock, sentinel.uuid)
75
# Avoid actual downloads:
76
self.patch_download(service, None)
77
maas_meta_last_modified = self.patch(
78
tftppath, 'maas_meta_last_modified')
79
maas_meta_last_modified.return_value = None
80
service.startService()
82
# The first call is issued at startup.
83
self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified)))
85
# Wind clock forward one second less than the desired interval.
86
clock.advance(service.check_interval - 1)
87
# No more periodic calls made.
88
self.assertEqual(1, len(get_mock_calls(maas_meta_last_modified)))
90
# Wind clock forward one second, past the interval.
93
# Now there were two calls.
94
self.assertEqual(2, len(get_mock_calls(maas_meta_last_modified)))
96
# Forward another interval, should be three calls.
97
clock.advance(service.check_interval)
98
self.assertEqual(3, len(get_mock_calls(maas_meta_last_modified)))
100
def test_initiates_download_if_no_meta_file(self):
102
service = ImageDownloadService(
103
sentinel.service, clock, sentinel.uuid)
104
_start_download = self.patch_download(service, None)
107
'maas_meta_last_modified').return_value = None
108
service.startService()
109
self.assertThat(_start_download, MockCalledOnceWith())
111
def test_initiates_download_if_15_minutes_has_passed(self):
113
service = ImageDownloadService(
114
sentinel.service, clock, sentinel.uuid)
115
_start_download = self.patch_download(service, None)
116
one_week_ago = clock.seconds() - timedelta(minutes=15).total_seconds()
119
'maas_meta_last_modified').return_value = one_week_ago
120
service.startService()
121
self.assertThat(_start_download, MockCalledOnceWith())
123
def test_no_download_if_15_minutes_has_not_passed(self):
125
service = ImageDownloadService(
126
sentinel.service, clock, sentinel.uuid)
127
_start_download = self.patch_download(service, None)
128
one_week = timedelta(minutes=15).total_seconds()
131
'maas_meta_last_modified').return_value = clock.seconds()
132
clock.advance(one_week - 1)
133
service.startService()
134
self.assertThat(_start_download, MockNotCalled())
136
def test_download_is_initiated_in_new_thread(self):
138
maas_meta_last_modified = self.patch(
139
tftppath, 'maas_meta_last_modified')
140
one_week = timedelta(minutes=15).total_seconds()
141
maas_meta_last_modified.return_value = clock.seconds() - one_week
144
client_call.side_effect = [
145
defer.succeed(dict(sources=sentinel.sources)),
147
rpc_client.getClient.return_value = client_call
149
# We could patch out 'import_boot_images' instead here but I
150
# don't do that for 2 reasons:
151
# 1. It requires spinning the reactor again before being able to
153
# 2. It means there's no thread to clean up after the test.
154
deferToThread = self.patch(boot_images, 'deferToThread')
155
deferToThread.return_value = defer.succeed(None)
156
service = ImageDownloadService(
157
rpc_client, clock, sentinel.uuid)
158
service.startService()
160
deferToThread, MockCalledOnceWith(
161
_run_import, sentinel.sources))
163
def test_no_download_if_no_rpc_connections(self):
165
failure = NoConnectionsAvailable()
166
rpc_client.getClient.side_effect = failure
168
deferToThread = self.patch(boot_images, 'deferToThread')
169
service = ImageDownloadService(
170
rpc_client, Clock(), sentinel.uuid)
171
service.startService()
172
self.assertThat(deferToThread, MockNotCalled())
174
def test_logs_other_errors(self):
175
service = ImageDownloadService(
176
sentinel.rpc, Clock(), sentinel.uuid)
178
maybe_start_download = self.patch(service, "maybe_start_download")
179
maybe_start_download.return_value = defer.fail(
180
ZeroDivisionError("Such a shame I can't divide by zero"))
182
with FakeLogger("maas") as maaslog, TwistedLoggerFixture():
183
d = service.try_download()
185
self.assertEqual(None, extract_result(d))
186
self.assertDocTestMatches(
187
"Failed to download images: "
188
"Such a shame I can't divide by zero",
192
class TestGetBootSources(PservTestCase):
194
run_tests_with = MAASTwistedRunTest.make_factory(timeout=5)
196
@defer.inlineCallbacks
197
def test__get_boot_sources_calls_get_boot_sources_v2_before_v1(self):
200
client_call.side_effect = [
201
defer.succeed(dict(sources=sentinel.sources)),
204
service = ImageDownloadService(
205
sentinel.rpc, clock, sentinel.uuid)
206
sources = yield service._get_boot_sources(client_call)
207
self.assertEqual(sources.get('sources'), sentinel.sources)
210
MockCalledOnceWith(GetBootSourcesV2, uuid=sentinel.uuid))
212
@defer.inlineCallbacks
213
def test__get_boot_sources_calls_get_boot_sources_v1_on_v2_missing(self):
216
client_call.side_effect = [
217
defer.fail(NoSuchMethod()),
218
defer.succeed(dict(sources=[])),
221
service = ImageDownloadService(
222
sentinel.rpc, clock, sentinel.uuid)
223
yield service._get_boot_sources(client_call)
227
call(GetBootSourcesV2, uuid=sentinel.uuid),
228
call(GetBootSources, uuid=sentinel.uuid)))
230
@defer.inlineCallbacks
231
def test__get_boot_sources_v1_sets_os_to_wildcard(self):
234
'path': factory.make_url(),
239
'subarches': ["generic"],
240
'labels': ["release"],
243
'release': "precise",
245
'subarches': ["generic"],
246
'labels': ["release"],
254
client_call.side_effect = [
255
defer.fail(NoSuchMethod()),
256
defer.succeed(dict(sources=sources)),
259
service = ImageDownloadService(
260
sentinel.rpc, clock, sentinel.uuid)
261
sources = yield service._get_boot_sources(client_call)
264
for source in sources['sources']
265
for selection in source['selections']
267
self.assertEqual(['*', '*'], os_selections)