2
"""Tests for the Model Migration feature"""
4
from __future__ import print_function
9
from subprocess import CalledProcessError
11
from time import sleep
13
from assess_user_grant_revoke import User
14
from deploy_stack import BootstrapManager
15
from jujucharm import local_charm_path
18
add_basic_testing_arguments,
28
log = logging.getLogger("assess_model_migration")
31
def assess_model_migration(bs1, bs2, upload_tools):
32
with bs1.booted_context(upload_tools):
33
bs1.client.enable_feature('migration')
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)
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)
48
"""Parse all arguments."""
49
parser = argparse.ArgumentParser(
50
description="Test model migration feature"
52
add_basic_testing_arguments(parser)
53
return parser.parse_args(argv)
56
def get_bootstrap_managers(args):
57
"""Create 2 bootstrap managers from the provided args.
59
Need to make a couple of elements uniqe (e.g. environment name) so we can
60
have 2 bootstrapped at the same time.
63
bs_1 = BootstrapManager.from_args(args)
64
bs_2 = BootstrapManager.from_args(args)
66
# Give the second a separate/unique name.
67
bs_2.temp_env_name = '{}-b'.format(bs_1.temp_env_name)
69
bs_1.log_dir = _new_log_dir(args.logs, 'a')
70
bs_2.log_dir = _new_log_dir(args.logs, 'b')
75
def _new_log_dir(log_dir, post_fix):
76
new_log_dir = os.path.join(log_dir, 'env-{}'.format(post_fix))
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
85
Defaults to 10 seconds timeout.
86
:raises AssertionError: If the named model does not appear in the specified
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']]:
97
raise JujuAssertionError(
98
'Model \'{}\' failed to appear after {} seconds'.format(
103
def test_deployed_mongo_is_up(client):
104
"""Ensure the mongo service is running as expected."""
106
output = client.get_juju_output(
107
'run', '--unit', 'mongodb/0', 'mongo --eval "db.getMongo()"')
108
if 'connecting to: test' in output:
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.')
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.
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.
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
137
application = 'mongodb'
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)
148
log.info('Initiating migration process')
150
migration_target_client = migrate_model_to_controller(
151
test_model, dest_environ.client)
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)
157
migration_target_client.remove_service(application)
160
def migrate_model_to_controller(source_client, dest_client):
161
source_client.controller_juju(
163
(source_client.env.environment,
164
dest_client.env.controller.name))
166
migration_target_client = dest_client.clone(
167
dest_client.env.clone(
168
source_client.env.environment))
171
migration_target_client, source_client.env.environment)
173
migration_target_client.wait_for_started()
175
return migration_target_client
178
def ensure_model_is_functional(client, application):
179
"""Ensures that the migrated model is functional
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
186
client.juju('add-unit', (application,))
187
client.wait_for_started()
189
assert_units_on_different_machines(client, application)
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()]
196
raise_if_shared_machines(unit_machines)
199
def raise_if_shared_machines(unit_machines):
200
"""Raise an exception if `unit_machines` contain double ups of machine ids.
202
A unique list of machine ids will be equal in length to the set of those
205
:raises ValueError: if an empty list is passed in.
206
:raises JujuAssertionError: if any double-ups of machine ids are detected.
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')
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.
219
A non-superuser on a controller cannot migrate their models between
223
source_client, dest_client = create_user_on_controllers(
224
source_bs, dest_bs, tmp_dir, 'failuser', 'addmodel')
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()
231
log.info('Attempting migration process')
233
expect_migration_attempt_to_fail(
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
242
A user with superuser permissions is able to migrate between controllers.
245
source_client, dest_client = create_user_on_controllers(
246
source_bs, dest_bs, tmp_dir, 'passuser', 'superuser')
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()
253
log.info('Attempting migration process')
255
migrate_model_to_controller(source_client, dest_client)
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)
269
second_controller_name = '{}_controllerb'.format(new_user.name)
270
dest_client = dest_bs.client.register_user(
273
controller_name=second_controller_name)
274
dest_bs.client.grant(new_user.name, permission)
276
source_client = normal_user_client_1.add_model(
277
normal_user_client_1.env.clone('model-a'))
279
return source_client, dest_client
282
def expect_migration_attempt_to_fail(source_client, dest_client):
283
"""Ensure that the migration attempt fails due to permissions.
285
As we're capturing the stderr output it after we're done with it so it
286
appears in test logs.
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:
299
log.info('SUCCESS: Migrate command failed as expected.')
301
print(log_output, file=sys.stderr)
302
raise JujuAssertionError('Migration did not fail as expected.')
306
args = parse_args(argv)
307
configure_logging(args.verbose)
309
bs1, bs2 = get_bootstrap_managers(args)
311
assess_model_migration(bs1, bs2, args.upload_tools)
316
if __name__ == '__main__':