1
# Copyright 2012-2014 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Tests for the `start_cluster_controller` command."""
6
from __future__ import (
17
from argparse import ArgumentParser
18
from collections import namedtuple
20
from io import BytesIO
28
from apiclient.maas_client import MAASDispatcher
29
from apiclient.testing.django import parse_headers_and_body_with_django
30
from fixtures import (
31
EnvironmentVariableFixture,
34
from maastesting.factory import factory
40
from provisioningserver import start_cluster_controller
41
from provisioningserver.testing.testcase import PservTestCase
42
from testtools.matchers import StartsWith
44
# Some tests in this file have to import methods from Django. This causes
45
# Django to parse its settings file and, in Django 1.5+, assert that it
46
# contains a value for the setting 'SECRET_KEY'.
47
# The trick we use here is to use this very module as Django's settings
48
# module and define a value for 'SECRET_KEY'.
49
SECRET_KEY = 'bogus secret key'
52
class Sleeping(Exception):
53
"""Exception: `sleep` has been called."""
56
class Executing(Exception):
57
"""Exception: an attempt has been made to start another process.
59
It would be inadvisable for tests in this test case to attempt to start
60
a real celeryd, so we want to know when it tries.
64
def make_url(name_hint='host'):
65
return "http://%s.example.com/%s/" % (
66
factory.make_name(name_hint),
67
factory.make_name('path'),
71
FakeArgs = namedtuple('FakeArgs', ['server_url', 'user', 'group'])
74
def make_args(server_url=None):
75
if server_url is None:
76
server_url = make_url('region')
77
user = factory.make_name('user')
78
group = factory.make_name('group')
79
return FakeArgs(server_url, user, group)
82
class FakeURLOpenResponse:
83
"""Cheap simile of a `urlopen` result."""
85
def __init__(self, content, status=httplib.OK):
86
self._content = content
87
self._status_code = status
93
return self._status_code
96
class TestStartClusterController(PservTestCase):
99
super(TestStartClusterController, self).setUp()
101
self.useFixture(FakeLogger())
102
self.patch(start_cluster_controller, 'set_up_logging')
104
# Patch out anything that could be remotely harmful if we did it
105
# accidentally in the test. Make the really outrageous ones
107
self.patch(start_cluster_controller, 'sleep').side_effect = Sleeping()
108
self.patch(start_cluster_controller, 'getpwnam')
109
self.patch(start_cluster_controller, 'getgrnam')
110
self.patch(os, 'setuid')
111
self.patch(os, 'setgid')
112
self.patch(os, 'execvpe').side_effect = Executing()
113
get_uuid = self.patch(start_cluster_controller, 'get_cluster_uuid')
114
get_uuid.return_value = factory.make_UUID()
116
def make_connection_details(self):
118
'BROKER_URL': make_url('broker'),
121
def parse_headers_and_body(self, headers, body):
122
"""Parse ingredients of a web request.
124
The headers and body are as passed to :class:`MAASDispatcher`.
126
# Make Django STFU; just using Django's multipart code causes it to
127
# pull in a settings module, and it will throw up if it can't.
129
EnvironmentVariableFixture(
130
"DJANGO_SETTINGS_MODULE", __name__))
132
post, files = parse_headers_and_body_with_django(headers, body)
135
def prepare_response(self, http_code, content=""):
136
"""Prepare to return the given http response from API request."""
137
fake = self.patch(MAASDispatcher, 'dispatch_query')
138
fake.return_value = FakeURLOpenResponse(content, status=http_code)
141
def prepare_success_response(self):
142
"""Prepare to return connection details from API request."""
143
details = self.make_connection_details()
144
self.prepare_response(httplib.OK, json.dumps(details))
147
def prepare_rejection_response(self):
148
"""Prepare to return "rejected" from API request."""
149
self.prepare_response(httplib.FORBIDDEN)
151
def prepare_pending_response(self):
152
"""Prepare to return "request pending" from API request."""
153
self.prepare_response(httplib.ACCEPTED)
155
def test_run_command(self):
156
# We can't really run the script, but we can verify that (with
157
# the right system functions patched out) we can run it
159
start_cluster_controller.sleep.side_effect = None
160
self.prepare_success_response()
161
parser = ArgumentParser()
162
start_cluster_controller.add_arguments(parser)
165
start_cluster_controller.run,
166
parser.parse_args((make_url(),)))
167
self.assertEqual(1, os.execvpe.call_count)
169
def test_uses_given_url(self):
170
url = make_url('region')
171
self.patch(start_cluster_controller, 'start_up')
172
self.prepare_success_response()
173
start_cluster_controller.run(make_args(server_url=url))
174
(args, kwargs) = MAASDispatcher.dispatch_query.call_args
175
self.assertThat(args[0], StartsWith(url + 'api/1.0/nodegroups/'))
177
def test_fails_if_declined(self):
178
self.patch(start_cluster_controller, 'start_up')
179
self.prepare_rejection_response()
181
start_cluster_controller.ClusterControllerRejected,
182
start_cluster_controller.run, make_args())
183
self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
185
def test_polls_while_pending(self):
186
self.patch(start_cluster_controller, 'start_up')
187
self.prepare_pending_response()
190
start_cluster_controller.run, make_args())
191
self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
193
def test_polls_on_unexpected_errors(self):
194
self.patch(start_cluster_controller, 'start_up')
195
self.patch(MAASDispatcher, 'dispatch_query').side_effect = HTTPError(
196
make_url(), httplib.REQUEST_TIMEOUT, "Timeout.", '', BytesIO())
199
start_cluster_controller.run, make_args())
200
self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
202
def test_register_passes_cluster_information(self):
203
self.prepare_success_response()
205
'interface': factory.make_name('eth'),
206
'ip': factory.getRandomIPAddress(),
207
'subnet_mask': '255.255.255.0',
209
discover = self.patch(start_cluster_controller, 'discover_networks')
210
discover.return_value = [interface]
212
start_cluster_controller.register(make_url())
214
(args, kwargs) = MAASDispatcher.dispatch_query.call_args
215
headers, body = kwargs["headers"], kwargs["data"]
216
post, files = self.parse_headers_and_body(headers, body)
217
self.assertEqual([interface], json.loads(post['interfaces']))
219
start_cluster_controller.get_cluster_uuid.return_value,
222
def test_starts_up_once_accepted(self):
223
self.patch(start_cluster_controller, 'start_up')
224
connection_details = self.prepare_success_response()
225
server_url = make_url()
226
start_cluster_controller.run(make_args(server_url=server_url))
227
self.assertItemsEqual(
228
start_cluster_controller.start_up.call_args[0],
229
(server_url, connection_details))
231
def test_start_up_calls_refresh_secrets(self):
232
start_cluster_controller.sleep.side_effect = None
233
url = make_url('region')
234
connection_details = self.make_connection_details()
235
self.prepare_success_response()
239
start_cluster_controller.start_up,
240
url, connection_details,
241
factory.make_name('user'), factory.make_name('group'))
243
(args, kwargs) = MAASDispatcher.dispatch_query.call_args
245
url + 'api/1.0/nodegroups/?op=refresh_workers', args[0])
246
self.assertEqual('POST', kwargs['method'])
248
headers, body = kwargs["headers"], kwargs["data"]
249
post, files = self.parse_headers_and_body(headers, body)
251
def test_start_up_ignores_failure_on_refresh_secrets(self):
252
start_cluster_controller.sleep.side_effect = None
253
self.patch(MAASDispatcher, 'dispatch_query').side_effect = URLError(
254
"Simulated HTTP failure.")
258
start_cluster_controller.start_up,
259
make_url(), self.make_connection_details(),
260
factory.make_name('user'), factory.make_name('group'))
262
self.assertEqual(1, os.execvpe.call_count)
264
def test_start_celery_sets_gid_before_uid(self):
265
# The gid should be changed before the uid; it may not be possible to
266
# change the gid once privileges are dropped.
267
start_cluster_controller.getpwnam.return_value.pw_uid = sentinel.uid
268
start_cluster_controller.getgrnam.return_value.gr_gid = sentinel.gid
269
# Patch setuid and setgid, using the same mock for both, so that we
270
# can observe call ordering.
271
setuidgid = self.patch(os, "setuid")
272
self.patch(os, "setgid", setuidgid)
274
Executing, start_cluster_controller.start_celery,
275
make_url(), self.make_connection_details(), sentinel.user,
277
# getpwname and getgrnam are used to query the passwd and group
278
# databases respectively.
280
[call(sentinel.user)],
281
start_cluster_controller.getpwnam.call_args_list)
283
[call(sentinel.group)],
284
start_cluster_controller.getgrnam.call_args_list)
285
# The arguments to the mocked setuid/setgid calls demonstrate that the
286
# gid was selected first.
288
[call(sentinel.gid), call(sentinel.uid)],
289
setuidgid.call_args_list)
291
def test_start_celery_passes_environment(self):
292
server_url = make_url()
293
connection_details = self.make_connection_details()
296
start_cluster_controller.start_celery,
297
server_url, connection_details, factory.make_name('user'),
298
factory.make_name('group'))
302
CELERY_BROKER_URL=connection_details['BROKER_URL'],
305
os.execvpe.assert_called_once_with(ANY, ANY, env=env)