~ubuntu-branches/ubuntu/vivid/maas/vivid

« back to all changes in this revision

Viewing changes to src/provisioningserver/tests/test_start_cluster_controller.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez, Jeroen Vermeulen, Andres Rodriguez, Jason Hobbs, Raphaël Badin, Louis Bouchard, Gavin Panella
  • Date: 2014-08-21 19:36:30 UTC
  • mfrom: (1.3.1)
  • Revision ID: package-import@ubuntu.com-20140821193630-kertpu5hd8yyss8h
Tags: 1.7.0~beta7+bzr3266-0ubuntu1
* New Upstream Snapshot, Beta 7 bzr3266

[ Jeroen Vermeulen ]
* debian/extras/99-maas-sudoers
  debian/maas-dhcp.postinst
  debian/rules
  - Add second DHCP server instance for IPv6.
* debian/maas-region-controller-min.install
  debian/maas-region-controller-min.lintian-overrides
  - Install deployment user-data: maas_configure_interfaces.py script.
* debian/maas-cluster-controller.links
  debian/maas-cluster-controller.install
  debian/maas-cluster-controller.postinst
  - Reflect Celery removal changes made in trunk r3067.
  - Don't install celeryconfig_cluster.py any longer. 
  - Don't install maas_local_celeryconfig_cluster.py any longer.
  - Don't symlink maas_local_celeryconfig_cluster.py from /etc to /usr.
  - Don't insert UUID into maas_local_celeryconfig_cluster.py.

[ Andres Rodriguez ]
* debian/maas-region-controller-min.postrm: Cleanup lefover files.
* debian/maas-dhcp.postrm: Clean leftover configs.
* Provide new maas-proxy package that replaces the usage of
  squid-deb-proxy:
  - debian/control: New maas-proxy package that replaces the usage
    of squid-deb-proxy; Drop depends on squid-deb-proxy.
  - Add upstrart job.
  - Ensure squid3 is stopped as maas-proxy uses a caching proxy.
* Remove Celery references to cluster controller:
  - Rename upstart job from maas-pserv to maas-cluster; rename
    maas-cluster-celery to maas-cluster-register. Ensure services
    are stopped on upgrade.
  - debian/maintscript: Cleanup config files.
  - Remove all references to the MAAS celery daemon and config
    files as we don't use it like that anymore
* Move some entries in debian/maintscript to
  debian/maas-cluster-controller.maintscript
* Remove usage of txlongpoll and rabbitmq-server. Handle upgrades
  to ensure these are removed correctly.

[ Jason Hobbs ]
* debian/maas-region-controller-min.install: Install
  maas-generate-winrm-cert script.

[ Raphaël Badin ]
* debian/extras/maas-region-admin: Bypass django-admin as it prints
  spurious messages to stdout (LP: #1365130).

[Louis Bouchard]
* debian/maas-cluster-controller.postinst:
  - Exclude /var/log/maas/rsyslog when changing ownership
    (LP: #1346703)

[Gavin Panella]
* debian/maas-cluster-controller.maas-clusterd.upstart:
  - Don't start-up the cluster controller unless a shared-secret has
    been installed.
* debian/maas-cluster-controller.maas-cluster-register.upstart: Drop.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2012-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 the `start_cluster_controller` command."""
5
 
 
6
 
from __future__ import (
7
 
    absolute_import,
8
 
    print_function,
9
 
    unicode_literals,
10
 
    )
11
 
 
12
 
str = None
13
 
 
14
 
__metaclass__ = type
15
 
__all__ = []
16
 
 
17
 
from argparse import ArgumentParser
18
 
from collections import namedtuple
19
 
import httplib
20
 
from io import BytesIO
21
 
import json
22
 
import os
23
 
from urllib2 import (
24
 
    HTTPError,
25
 
    URLError,
26
 
    )
27
 
 
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,
32
 
    FakeLogger,
33
 
    )
34
 
from maastesting.factory import factory
35
 
from mock import (
36
 
    ANY,
37
 
    call,
38
 
    sentinel,
39
 
    )
40
 
from provisioningserver import start_cluster_controller
41
 
from provisioningserver.testing.testcase import PservTestCase
42
 
from testtools.matchers import StartsWith
43
 
 
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'
50
 
 
51
 
 
52
 
class Sleeping(Exception):
53
 
    """Exception: `sleep` has been called."""
54
 
 
55
 
 
56
 
class Executing(Exception):
57
 
    """Exception: an attempt has been made to start another process.
58
 
 
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.
61
 
    """
62
 
 
63
 
 
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'),
68
 
        )
69
 
 
70
 
 
71
 
FakeArgs = namedtuple('FakeArgs', ['server_url', 'user', 'group'])
72
 
 
73
 
 
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)
80
 
 
81
 
 
82
 
class FakeURLOpenResponse:
83
 
    """Cheap simile of a `urlopen` result."""
84
 
 
85
 
    def __init__(self, content, status=httplib.OK):
86
 
        self._content = content
87
 
        self._status_code = status
88
 
 
89
 
    def read(self):
90
 
        return self._content
91
 
 
92
 
    def getcode(self):
93
 
        return self._status_code
94
 
 
95
 
 
96
 
class TestStartClusterController(PservTestCase):
97
 
 
98
 
    def setUp(self):
99
 
        super(TestStartClusterController, self).setUp()
100
 
 
101
 
        self.useFixture(FakeLogger())
102
 
        self.patch(start_cluster_controller, 'set_up_logging')
103
 
 
104
 
        # Patch out anything that could be remotely harmful if we did it
105
 
        # accidentally in the test.  Make the really outrageous ones
106
 
        # raise exceptions.
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()
115
 
 
116
 
    def make_connection_details(self):
117
 
        return {
118
 
            'BROKER_URL': make_url('broker'),
119
 
        }
120
 
 
121
 
    def parse_headers_and_body(self, headers, body):
122
 
        """Parse ingredients of a web request.
123
 
 
124
 
        The headers and body are as passed to :class:`MAASDispatcher`.
125
 
        """
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.
128
 
        self.useFixture(
129
 
            EnvironmentVariableFixture(
130
 
                "DJANGO_SETTINGS_MODULE", __name__))
131
 
 
132
 
        post, files = parse_headers_and_body_with_django(headers, body)
133
 
        return post, files
134
 
 
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)
139
 
        return fake
140
 
 
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))
145
 
        return details
146
 
 
147
 
    def prepare_rejection_response(self):
148
 
        """Prepare to return "rejected" from API request."""
149
 
        self.prepare_response(httplib.FORBIDDEN)
150
 
 
151
 
    def prepare_pending_response(self):
152
 
        """Prepare to return "request pending" from API request."""
153
 
        self.prepare_response(httplib.ACCEPTED)
154
 
 
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
158
 
        # directly.
159
 
        start_cluster_controller.sleep.side_effect = None
160
 
        self.prepare_success_response()
161
 
        parser = ArgumentParser()
162
 
        start_cluster_controller.add_arguments(parser)
163
 
        self.assertRaises(
164
 
            Executing,
165
 
            start_cluster_controller.run,
166
 
            parser.parse_args((make_url(),)))
167
 
        self.assertEqual(1, os.execvpe.call_count)
168
 
 
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/'))
176
 
 
177
 
    def test_fails_if_declined(self):
178
 
        self.patch(start_cluster_controller, 'start_up')
179
 
        self.prepare_rejection_response()
180
 
        self.assertRaises(
181
 
            start_cluster_controller.ClusterControllerRejected,
182
 
            start_cluster_controller.run, make_args())
183
 
        self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
184
 
 
185
 
    def test_polls_while_pending(self):
186
 
        self.patch(start_cluster_controller, 'start_up')
187
 
        self.prepare_pending_response()
188
 
        self.assertRaises(
189
 
            Sleeping,
190
 
            start_cluster_controller.run, make_args())
191
 
        self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
192
 
 
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())
197
 
        self.assertRaises(
198
 
            Sleeping,
199
 
            start_cluster_controller.run, make_args())
200
 
        self.assertItemsEqual([], start_cluster_controller.start_up.calls_list)
201
 
 
202
 
    def test_register_passes_cluster_information(self):
203
 
        self.prepare_success_response()
204
 
        interface = {
205
 
            'interface': factory.make_name('eth'),
206
 
            'ip': factory.getRandomIPAddress(),
207
 
            'subnet_mask': '255.255.255.0',
208
 
            }
209
 
        discover = self.patch(start_cluster_controller, 'discover_networks')
210
 
        discover.return_value = [interface]
211
 
 
212
 
        start_cluster_controller.register(make_url())
213
 
 
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']))
218
 
        self.assertEqual(
219
 
            start_cluster_controller.get_cluster_uuid.return_value,
220
 
            post['uuid'])
221
 
 
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))
230
 
 
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()
236
 
 
237
 
        self.assertRaises(
238
 
            Executing,
239
 
            start_cluster_controller.start_up,
240
 
            url, connection_details,
241
 
            factory.make_name('user'), factory.make_name('group'))
242
 
 
243
 
        (args, kwargs) = MAASDispatcher.dispatch_query.call_args
244
 
        self.assertEqual(
245
 
            url + 'api/1.0/nodegroups/?op=refresh_workers', args[0])
246
 
        self.assertEqual('POST', kwargs['method'])
247
 
 
248
 
        headers, body = kwargs["headers"], kwargs["data"]
249
 
        post, files = self.parse_headers_and_body(headers, body)
250
 
 
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.")
255
 
 
256
 
        self.assertRaises(
257
 
            Executing,
258
 
            start_cluster_controller.start_up,
259
 
            make_url(), self.make_connection_details(),
260
 
            factory.make_name('user'), factory.make_name('group'))
261
 
 
262
 
        self.assertEqual(1, os.execvpe.call_count)
263
 
 
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)
273
 
        self.assertRaises(
274
 
            Executing, start_cluster_controller.start_celery,
275
 
            make_url(), self.make_connection_details(), sentinel.user,
276
 
            sentinel.group)
277
 
        # getpwname and getgrnam are used to query the passwd and group
278
 
        # databases respectively.
279
 
        self.assertEqual(
280
 
            [call(sentinel.user)],
281
 
            start_cluster_controller.getpwnam.call_args_list)
282
 
        self.assertEqual(
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.
287
 
        self.assertEqual(
288
 
            [call(sentinel.gid), call(sentinel.uid)],
289
 
            setuidgid.call_args_list)
290
 
 
291
 
    def test_start_celery_passes_environment(self):
292
 
        server_url = make_url()
293
 
        connection_details = self.make_connection_details()
294
 
        self.assertRaises(
295
 
            Executing,
296
 
            start_cluster_controller.start_celery,
297
 
            server_url, connection_details, factory.make_name('user'),
298
 
            factory.make_name('group'))
299
 
 
300
 
        env = dict(
301
 
            os.environ,
302
 
            CELERY_BROKER_URL=connection_details['BROKER_URL'],
303
 
            MAAS_URL=server_url,
304
 
            )
305
 
        os.execvpe.assert_called_once_with(ANY, ANY, env=env)