~andrewjbeach/juju-ci-tools/make-local-patcher

« back to all changes in this revision

Viewing changes to assess_model_migration.py

  • Committer: Curtis Hovey
  • Date: 2016-09-21 17:54:26 UTC
  • mto: This revision was merged to the branch mainline in revision 1612.
  • Revision ID: curtis@canonical.com-20160921175426-hmk3wlxrl1vpfuwr
Poll for the token, whcih might be None.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
"""Tests for the Model Migration feature"""
 
3
 
 
4
from __future__ import print_function
 
5
 
 
6
import argparse
 
7
import logging
 
8
import os
 
9
from subprocess import CalledProcessError
 
10
import sys
 
11
from time import sleep
 
12
 
 
13
from assess_user_grant_revoke import User
 
14
from deploy_stack import BootstrapManager
 
15
from jujucharm import local_charm_path
 
16
from utility import (
 
17
    JujuAssertionError,
 
18
    add_basic_testing_arguments,
 
19
    configure_logging,
 
20
    temp_dir,
 
21
    until_timeout,
 
22
)
 
23
 
 
24
 
 
25
__metaclass__ = type
 
26
 
 
27
 
 
28
log = logging.getLogger("assess_model_migration")
 
29
 
 
30
 
 
31
def assess_model_migration(bs1, bs2, upload_tools):
 
32
    with bs1.booted_context(upload_tools):
 
33
        bs1.client.enable_feature('migration')
 
34
 
 
35
        bs2.client.env.juju_home = bs1.client.env.juju_home
 
36
        with bs2.existing_booted_context(upload_tools):
 
37
            ensure_able_to_migrate_model_between_controllers(
 
38
                bs1, bs2, upload_tools)
 
39
 
 
40
            with temp_dir() as temp:
 
41
                ensure_migrating_with_insufficient_user_permissions_fails(
 
42
                    bs1, bs2, upload_tools, temp)
 
43
                ensure_migrating_with_superuser_user_permissions_succeeds(
 
44
                    bs1, bs2, upload_tools, temp)
 
45
 
 
46
 
 
47
def parse_args(argv):
 
48
    """Parse all arguments."""
 
49
    parser = argparse.ArgumentParser(
 
50
        description="Test model migration feature"
 
51
    )
 
52
    add_basic_testing_arguments(parser)
 
53
    return parser.parse_args(argv)
 
54
 
 
55
 
 
56
def get_bootstrap_managers(args):
 
57
    """Create 2 bootstrap managers from the provided args.
 
58
 
 
59
    Need to make a couple of elements uniqe (e.g. environment name) so we can
 
60
    have 2 bootstrapped at the same time.
 
61
 
 
62
    """
 
63
    bs_1 = BootstrapManager.from_args(args)
 
64
    bs_2 = BootstrapManager.from_args(args)
 
65
 
 
66
    # Give the second a separate/unique name.
 
67
    bs_2.temp_env_name = '{}-b'.format(bs_1.temp_env_name)
 
68
 
 
69
    bs_1.log_dir = _new_log_dir(args.logs, 'a')
 
70
    bs_2.log_dir = _new_log_dir(args.logs, 'b')
 
71
 
 
72
    return bs_1, bs_2
 
73
 
 
74
 
 
75
def _new_log_dir(log_dir, post_fix):
 
76
    new_log_dir = os.path.join(log_dir, 'env-{}'.format(post_fix))
 
77
    os.mkdir(new_log_dir)
 
78
    return new_log_dir
 
79
 
 
80
 
 
81
def wait_for_model(client, model_name, timeout=60):
 
82
    """Wait for a given `timeout` for a model of `model_name` to appear within
 
83
    `client`.
 
84
 
 
85
    Defaults to 10 seconds timeout.
 
86
    :raises AssertionError: If the named model does not appear in the specified
 
87
      timeout.
 
88
 
 
89
    """
 
90
    with client.check_timeouts():
 
91
        with client.ignore_soft_deadline():
 
92
            for _ in until_timeout(timeout):
 
93
                models = client.get_models()
 
94
                if model_name in [m['name'] for m in models['models']]:
 
95
                    return
 
96
                sleep(1)
 
97
            raise JujuAssertionError(
 
98
                'Model \'{}\' failed to appear after {} seconds'.format(
 
99
                    model_name, timeout
 
100
                ))
 
101
 
 
102
 
 
103
def test_deployed_mongo_is_up(client):
 
104
    """Ensure the mongo service is running as expected."""
 
105
    try:
 
106
        output = client.get_juju_output(
 
107
            'run', '--unit', 'mongodb/0', 'mongo --eval "db.getMongo()"')
 
108
        if 'connecting to: test' in output:
 
109
            return
 
110
    except CalledProcessError as e:
 
111
        # Pass through to assertion error
 
112
        log.error('Mongodb check command failed: {}'.format(e))
 
113
    raise AssertionError('Mongo db is not in an expected state.')
 
114
 
 
115
 
 
116
def ensure_able_to_migrate_model_between_controllers(
 
117
        source_environ, dest_environ, upload_tools):
 
118
    """Test simple migration of a model to another controller.
 
119
 
 
120
    Ensure that migration a model that has an application deployed upon it is
 
121
    able to continue it's operation after the migration process.
 
122
 
 
123
    Given 2 bootstrapped environments:
 
124
      - Deploy an application
 
125
        - ensure it's operating as expected
 
126
      - Migrate that model to the other environment
 
127
        - Ensure it's operating as expected
 
128
        - Add a new unit to the application to ensure the model is functional
 
129
      - Migrate the model back to the original environment
 
130
        - Note: Test for lp:1607457
 
131
        - Ensure it's operating as expected
 
132
        - Add a new unit to the application to ensure the model is functional
 
133
 
 
134
 
 
135
    """
 
136
    bundle = 'mongodb'
 
137
    application = 'mongodb'
 
138
 
 
139
    log.info('Deploying charm')
 
140
    # Don't move the default model so we can reuse it in later tests.
 
141
    test_model = source_environ.client.add_model(
 
142
        source_environ.client.env.clone('example-model'))
 
143
    test_model.juju("deploy", (bundle))
 
144
    test_model.wait_for_started()
 
145
    test_model.wait_for_workloads()
 
146
    test_deployed_mongo_is_up(test_model)
 
147
 
 
148
    log.info('Initiating migration process')
 
149
 
 
150
    migration_target_client = migrate_model_to_controller(
 
151
        test_model, dest_environ.client)
 
152
 
 
153
    migration_target_client.wait_for_workloads()
 
154
    test_deployed_mongo_is_up(migration_target_client)
 
155
    ensure_model_is_functional(migration_target_client, application)
 
156
 
 
157
    migration_target_client.remove_service(application)
 
158
 
 
159
 
 
160
def migrate_model_to_controller(source_client, dest_client):
 
161
    source_client.controller_juju(
 
162
        'migrate',
 
163
        (source_client.env.environment,
 
164
         dest_client.env.controller.name))
 
165
 
 
166
    migration_target_client = dest_client.clone(
 
167
        dest_client.env.clone(
 
168
            source_client.env.environment))
 
169
 
 
170
    wait_for_model(
 
171
        migration_target_client, source_client.env.environment)
 
172
 
 
173
    migration_target_client.wait_for_started()
 
174
 
 
175
    return migration_target_client
 
176
 
 
177
 
 
178
def ensure_model_is_functional(client, application):
 
179
    """Ensures that the migrated model is functional
 
180
 
 
181
    Add unit to application to ensure the model is contactable and working.
 
182
    Ensure that added unit is created on a new machine (check for bug
 
183
    LP:1607599)
 
184
 
 
185
    """
 
186
    client.juju('add-unit', (application,))
 
187
    client.wait_for_started()
 
188
 
 
189
    assert_units_on_different_machines(client, application)
 
190
 
 
191
 
 
192
def assert_units_on_different_machines(client, application):
 
193
    status = client.get_status()
 
194
    unit_machines = [u[1]['machine'] for u in status.iter_units()]
 
195
 
 
196
    raise_if_shared_machines(unit_machines)
 
197
 
 
198
 
 
199
def raise_if_shared_machines(unit_machines):
 
200
    """Raise an exception if `unit_machines` contain double ups of machine ids.
 
201
 
 
202
    A unique list of machine ids will be equal in length to the set of those
 
203
    machine ids.
 
204
 
 
205
    :raises ValueError: if an empty list is passed in.
 
206
    :raises JujuAssertionError: if any double-ups of machine ids are detected.
 
207
 
 
208
    """
 
209
    if not unit_machines:
 
210
        raise ValueError('Cannot share 0 machines. Empty list provided.')
 
211
    if len(unit_machines) != len(set(unit_machines)):
 
212
        raise JujuAssertionError('Appliction units reside on the same machine')
 
213
 
 
214
 
 
215
def ensure_migrating_with_insufficient_user_permissions_fails(
 
216
        source_bs, dest_bs, upload_tools, tmp_dir):
 
217
    """Ensure migration fails when a user does not have the right permissions.
 
218
 
 
219
    A non-superuser on a controller cannot migrate their models between
 
220
    controllers.
 
221
 
 
222
    """
 
223
    source_client, dest_client = create_user_on_controllers(
 
224
        source_bs, dest_bs, tmp_dir, 'failuser', 'addmodel')
 
225
 
 
226
    charm_path = local_charm_path(
 
227
        charm='dummy-source', juju_ver=source_client.version)
 
228
    source_client.deploy(charm_path)
 
229
    source_client.wait_for_started()
 
230
 
 
231
    log.info('Attempting migration process')
 
232
 
 
233
    expect_migration_attempt_to_fail(
 
234
        source_client,
 
235
        dest_client)
 
236
 
 
237
 
 
238
def ensure_migrating_with_superuser_user_permissions_succeeds(
 
239
        source_bs, dest_bs, upload_tools, tmp_dir):
 
240
    """Ensure migration succeeds when a user has superuser permissions
 
241
 
 
242
    A user with superuser permissions is able to migrate between controllers.
 
243
 
 
244
    """
 
245
    source_client, dest_client = create_user_on_controllers(
 
246
        source_bs, dest_bs, tmp_dir, 'passuser', 'superuser')
 
247
 
 
248
    charm_path = local_charm_path(
 
249
        charm='dummy-source', juju_ver=source_client.version)
 
250
    source_client.deploy(charm_path)
 
251
    source_client.wait_for_started()
 
252
 
 
253
    log.info('Attempting migration process')
 
254
 
 
255
    migrate_model_to_controller(source_client, dest_client)
 
256
 
 
257
 
 
258
def create_user_on_controllers(
 
259
        source_bs, dest_bs, tmp_dir, username, permission):
 
260
    # Create a user for both controllers that only has addmodel
 
261
    # permissions not superuser.
 
262
    new_user_home = os.path.join(tmp_dir, username)
 
263
    os.makedirs(new_user_home)
 
264
    new_user = User(username, 'write', [])
 
265
    normal_user_client_1 = source_bs.client.register_user(
 
266
        new_user, new_user_home)
 
267
    source_bs.client.grant(new_user.name, permission)
 
268
 
 
269
    second_controller_name = '{}_controllerb'.format(new_user.name)
 
270
    dest_client = dest_bs.client.register_user(
 
271
        new_user,
 
272
        new_user_home,
 
273
        controller_name=second_controller_name)
 
274
    dest_bs.client.grant(new_user.name, permission)
 
275
 
 
276
    source_client = normal_user_client_1.add_model(
 
277
        normal_user_client_1.env.clone('model-a'))
 
278
 
 
279
    return source_client, dest_client
 
280
 
 
281
 
 
282
def expect_migration_attempt_to_fail(source_client, dest_client):
 
283
    """Ensure that the migration attempt fails due to permissions.
 
284
 
 
285
    As we're capturing the stderr output it after we're done with it so it
 
286
    appears in test logs.
 
287
 
 
288
    """
 
289
    try:
 
290
        args = ['-c', source_client.env.controller.name,
 
291
                source_client.env.environment,
 
292
                dest_client.env.controller.name]
 
293
        log_output = source_client.get_juju_output(
 
294
            'migrate', *args, merge_stderr=True, include_e=False)
 
295
    except CalledProcessError as e:
 
296
        print(e.output, file=sys.stderr)
 
297
        if 'permission denied' not in e.output:
 
298
            raise
 
299
        log.info('SUCCESS: Migrate command failed as expected.')
 
300
    else:
 
301
        print(log_output, file=sys.stderr)
 
302
        raise JujuAssertionError('Migration did not fail as expected.')
 
303
 
 
304
 
 
305
def main(argv=None):
 
306
    args = parse_args(argv)
 
307
    configure_logging(args.verbose)
 
308
 
 
309
    bs1, bs2 = get_bootstrap_managers(args)
 
310
 
 
311
    assess_model_migration(bs1, bs2, args.upload_tools)
 
312
 
 
313
    return 0
 
314
 
 
315
 
 
316
if __name__ == '__main__':
 
317
    sys.exit(main())