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

« back to all changes in this revision

Viewing changes to fakejuju.py

  • Committer: Martin Packman
  • Date: 2016-04-29 00:19:30 UTC
  • mto: This revision was merged to the branch mainline in revision 1389.
  • Revision ID: martin.packman@canonical.com-20160429001930-yoju030ik6lwqf0e
Add clean_maas.py script for releasing machines

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
from argparse import ArgumentParser
2
 
from base64 import b64encode
3
 
from contextlib import contextmanager
4
 
import copy
5
 
from hashlib import sha512
6
 
from itertools import count
7
 
import json
8
 
import logging
9
 
import subprocess
10
 
 
11
 
import yaml
12
 
 
13
 
from jujupy import (
14
 
    EnvJujuClient,
15
 
    JESNotSupported,
16
 
    JujuData,
17
 
)
18
 
 
19
 
__metaclass__ = type
20
 
 
21
 
 
22
 
def check_juju_output(func):
23
 
    def wrapper(*args, **kwargs):
24
 
        result = func(*args, **kwargs)
25
 
        if 'service' in result:
26
 
            raise AssertionError('Result contained service')
27
 
        return result
28
 
    return wrapper
29
 
 
30
 
 
31
 
class ControllerOperation(Exception):
32
 
 
33
 
    def __init__(self, operation):
34
 
        super(ControllerOperation, self).__init__(
35
 
            'Operation "{}" is only valid on controller models.'.format(
36
 
                operation))
37
 
 
38
 
 
39
 
def assert_juju_call(test_case, mock_method, client, expected_args,
40
 
                     call_index=None):
41
 
    if call_index is None:
42
 
        test_case.assertEqual(len(mock_method.mock_calls), 1)
43
 
        call_index = 0
44
 
    empty, args, kwargs = mock_method.mock_calls[call_index]
45
 
    test_case.assertEqual(args, (expected_args,))
46
 
 
47
 
 
48
 
class FakeControllerState:
49
 
 
50
 
    def __init__(self):
51
 
        self.state = 'not-bootstrapped'
52
 
        self.models = {}
53
 
        self.users = {
54
 
            'admin': {
55
 
                'state': '',
56
 
                'permission': 'write'
57
 
            }
58
 
        }
59
 
        self.shares = ['admin']
60
 
 
61
 
    def add_model(self, name):
62
 
        state = FakeEnvironmentState(self)
63
 
        state.name = name
64
 
        self.models[name] = state
65
 
        state.controller.state = 'created'
66
 
        return state
67
 
 
68
 
    def require_controller(self, operation, name):
69
 
        if name != self.controller_model.name:
70
 
            raise ControllerOperation(operation)
71
 
 
72
 
    def grant(self, username, permission):
73
 
        model_permissions = ['read', 'write', 'admin']
74
 
        if permission in model_permissions:
75
 
            permission = 'login'
76
 
        self.users[username]['access'] = permission
77
 
 
78
 
    def add_user_perms(self, username, permissions):
79
 
        self.users.update(
80
 
            {username: {'state': '', 'permission': permissions}})
81
 
        self.shares.append(username)
82
 
 
83
 
    def bootstrap(self, model_name, config, separate_controller):
84
 
        default_model = self.add_model(model_name)
85
 
        default_model.name = model_name
86
 
        if separate_controller:
87
 
            controller_model = default_model.controller.add_model('controller')
88
 
        else:
89
 
            controller_model = default_model
90
 
        self.controller_model = controller_model
91
 
        controller_model.state_servers.append(controller_model.add_machine())
92
 
        self.state = 'bootstrapped'
93
 
        default_model.model_config = copy.deepcopy(config)
94
 
        self.models[default_model.name] = default_model
95
 
        return default_model
96
 
 
97
 
 
98
 
class FakeEnvironmentState:
99
 
    """A Fake environment state that can be used by multiple FakeBackends."""
100
 
 
101
 
    def __init__(self, controller=None):
102
 
        self._clear()
103
 
        if controller is not None:
104
 
            self.controller = controller
105
 
        else:
106
 
            self.controller = FakeControllerState()
107
 
 
108
 
    def _clear(self):
109
 
        self.name = None
110
 
        self.machine_id_iter = count()
111
 
        self.state_servers = []
112
 
        self.services = {}
113
 
        self.machines = set()
114
 
        self.containers = {}
115
 
        self.relations = {}
116
 
        self.token = None
117
 
        self.exposed = set()
118
 
        self.machine_host_names = {}
119
 
        self.current_bundle = None
120
 
        self.model_config = None
121
 
        self.ssh_keys = []
122
 
 
123
 
    @property
124
 
    def state(self):
125
 
        return self.controller.state
126
 
 
127
 
    def add_machine(self, host_name=None, machine_id=None):
128
 
        if machine_id is None:
129
 
            machine_id = str(self.machine_id_iter.next())
130
 
        self.machines.add(machine_id)
131
 
        if host_name is None:
132
 
            host_name = '{}.example.com'.format(machine_id)
133
 
        self.machine_host_names[machine_id] = host_name
134
 
        return machine_id
135
 
 
136
 
    def add_ssh_machines(self, machines):
137
 
        for machine in machines:
138
 
            self.add_machine()
139
 
 
140
 
    def add_container(self, container_type, host=None, container_num=None):
141
 
        if host is None:
142
 
            host = self.add_machine()
143
 
        host_containers = self.containers.setdefault(host, set())
144
 
        if container_num is None:
145
 
            same_type_containers = [x for x in host_containers if
146
 
                                    container_type in x]
147
 
            container_num = len(same_type_containers)
148
 
        container_name = '{}/{}/{}'.format(host, container_type, container_num)
149
 
        host_containers.add(container_name)
150
 
        host_name = '{}.example.com'.format(container_name)
151
 
        self.machine_host_names[container_name] = host_name
152
 
 
153
 
    def remove_container(self, container_id):
154
 
        for containers in self.containers.values():
155
 
            containers.discard(container_id)
156
 
 
157
 
    def remove_machine(self, machine_id):
158
 
        self.machines.remove(machine_id)
159
 
        self.containers.pop(machine_id, None)
160
 
 
161
 
    def remove_state_server(self, machine_id):
162
 
        self.remove_machine(machine_id)
163
 
        self.state_servers.remove(machine_id)
164
 
 
165
 
    def destroy_environment(self):
166
 
        self._clear()
167
 
        self.controller.state = 'destroyed'
168
 
        return 0
169
 
 
170
 
    def kill_controller(self):
171
 
        self._clear()
172
 
        self.controller.state = 'controller-killed'
173
 
 
174
 
    def destroy_model(self):
175
 
        del self.controller.models[self.name]
176
 
        self._clear()
177
 
        self.controller.state = 'model-destroyed'
178
 
 
179
 
    def _fail_stderr(self, message, returncode=1, cmd='juju', stdout=''):
180
 
        exc = subprocess.CalledProcessError(returncode, cmd, stdout)
181
 
        exc.stderr = message
182
 
        raise exc
183
 
 
184
 
    def restore_backup(self):
185
 
        self.controller.require_controller('restore', self.name)
186
 
        if len(self.state_servers) > 0:
187
 
            return self._fail_stderr('Operation not permitted')
188
 
        self.state_servers.append(self.add_machine())
189
 
 
190
 
    def enable_ha(self):
191
 
        self.controller.require_controller('enable-ha', self.name)
192
 
        for n in range(2):
193
 
            self.state_servers.append(self.add_machine())
194
 
 
195
 
    def deploy(self, charm_name, service_name):
196
 
        self.add_unit(service_name)
197
 
 
198
 
    def deploy_bundle(self, bundle_path):
199
 
        self.current_bundle = bundle_path
200
 
 
201
 
    def add_unit(self, service_name):
202
 
        machines = self.services.setdefault(service_name, set())
203
 
        machines.add(
204
 
            ('{}/{}'.format(service_name, str(len(machines))),
205
 
             self.add_machine()))
206
 
 
207
 
    def remove_unit(self, to_remove):
208
 
        for units in self.services.values():
209
 
            for unit_id, machine_id in units:
210
 
                if unit_id == to_remove:
211
 
                    self.remove_machine(machine_id)
212
 
                    units.remove((unit_id, machine_id))
213
 
                    break
214
 
 
215
 
    def destroy_service(self, service_name):
216
 
        for unit, machine_id in self.services.pop(service_name):
217
 
            self.remove_machine(machine_id)
218
 
 
219
 
    def get_status_dict(self):
220
 
        machines = {}
221
 
        for machine_id in self.machines:
222
 
            machine_dict = {'juju-status': {'current': 'idle'}}
223
 
            hostname = self.machine_host_names.get(machine_id)
224
 
            machine_dict['instance-id'] = machine_id
225
 
            if hostname is not None:
226
 
                machine_dict['dns-name'] = hostname
227
 
            machines[machine_id] = machine_dict
228
 
            if machine_id in self.state_servers:
229
 
                machine_dict['controller-member-status'] = 'has-vote'
230
 
        for host, containers in self.containers.items():
231
 
            container_dict = dict((c, {}) for c in containers)
232
 
            for container in containers:
233
 
                dns_name = self.machine_host_names.get(container)
234
 
                if dns_name is not None:
235
 
                    container_dict[container]['dns-name'] = dns_name
236
 
 
237
 
            machines[host]['containers'] = container_dict
238
 
        services = {}
239
 
        for service, units in self.services.items():
240
 
            unit_map = {}
241
 
            for unit_id, machine_id in units:
242
 
                unit_map[unit_id] = {
243
 
                    'machine': machine_id,
244
 
                    'juju-status': {'current': 'idle'}}
245
 
            services[service] = {
246
 
                'units': unit_map,
247
 
                'relations': self.relations.get(service, {}),
248
 
                'exposed': service in self.exposed,
249
 
                }
250
 
        return {'machines': machines, 'applications': services}
251
 
 
252
 
    def add_ssh_key(self, keys_to_add):
253
 
        errors = []
254
 
        for key in keys_to_add:
255
 
            if not key.startswith("ssh-rsa "):
256
 
                errors.append(
257
 
                    'cannot add key "{0}": invalid ssh key: {0}'.format(key))
258
 
            elif key in self.ssh_keys:
259
 
                errors.append(
260
 
                    'cannot add key "{0}": duplicate ssh key: {0}'.format(key))
261
 
            else:
262
 
                self.ssh_keys.append(key)
263
 
        return '\n'.join(errors)
264
 
 
265
 
    def remove_ssh_key(self, keys_to_remove):
266
 
        errors = []
267
 
        for i in reversed(range(len(keys_to_remove))):
268
 
            key = keys_to_remove[i]
269
 
            if key in ('juju-client-key', 'juju-system-key'):
270
 
                keys_to_remove = keys_to_remove[:i] + keys_to_remove[i + 1:]
271
 
                errors.append(
272
 
                    'cannot remove key id "{0}": may not delete internal key:'
273
 
                    ' {0}'.format(key))
274
 
        for i in range(len(self.ssh_keys)):
275
 
            if self.ssh_keys[i] in keys_to_remove:
276
 
                keys_to_remove.remove(self.ssh_keys[i])
277
 
                del self.ssh_keys[i]
278
 
        errors.extend(
279
 
            'cannot remove key id "{0}": invalid ssh key: {0}'.format(key)
280
 
            for key in keys_to_remove)
281
 
        return '\n'.join(errors)
282
 
 
283
 
    def import_ssh_key(self, names_to_add):
284
 
        for name in names_to_add:
285
 
            self.ssh_keys.append('ssh-rsa FAKE_KEY a key {}'.format(name))
286
 
        return ""
287
 
 
288
 
 
289
 
class FakeExpectChild:
290
 
 
291
 
    def __init__(self, backend, juju_home, extra_env):
292
 
        self.backend = backend
293
 
        self.juju_home = juju_home
294
 
        self.extra_env = extra_env
295
 
        self.last_expect = None
296
 
        self.exitstatus = None
297
 
 
298
 
    def expect(self, line):
299
 
        self.last_expect = line
300
 
 
301
 
    def sendline(self, line):
302
 
        """Do-nothing implementation of sendline.
303
 
 
304
 
        Subclassess will likely override this.
305
 
        """
306
 
 
307
 
    def close(self):
308
 
        self.exitstatus = 0
309
 
 
310
 
    def isalive(self):
311
 
        return bool(self.exitstatus is not None)
312
 
 
313
 
 
314
 
class AutoloadCredentials(FakeExpectChild):
315
 
 
316
 
    def __init__(self, backend, juju_home, extra_env):
317
 
        super(AutoloadCredentials, self).__init__(backend, juju_home,
318
 
                                                  extra_env)
319
 
        self.cloud = None
320
 
 
321
 
    def sendline(self, line):
322
 
        if self.last_expect == (
323
 
                '(Select the cloud it belongs to|'
324
 
                'Enter cloud to which the credential).* Q to quit.*'):
325
 
            self.cloud = line
326
 
 
327
 
    def isalive(self):
328
 
        juju_data = JujuData('foo', juju_home=self.juju_home)
329
 
        juju_data.load_yaml()
330
 
        creds = juju_data.credentials.setdefault('credentials', {})
331
 
        creds.update({self.cloud: {
332
 
            'default-region': self.extra_env['OS_REGION_NAME'],
333
 
            self.extra_env['OS_USERNAME']: {
334
 
                'domain-name': '',
335
 
                'auth-type': 'userpass',
336
 
                'username': self.extra_env['OS_USERNAME'],
337
 
                'password': self.extra_env['OS_PASSWORD'],
338
 
                'tenant-name': self.extra_env['OS_TENANT_NAME'],
339
 
                }}})
340
 
        juju_data.dump_yaml(self.juju_home, {})
341
 
        return False
342
 
 
343
 
 
344
 
class FakeBackend:
345
 
    """A fake juju backend for tests.
346
 
 
347
 
    This is a partial implementation, but should be suitable for many uses,
348
 
    and can be extended.
349
 
 
350
 
    The state is provided by controller_state, so that multiple clients and
351
 
    backends can manipulate the same state.
352
 
    """
353
 
 
354
 
    def __init__(self, controller_state, feature_flags=None, version=None,
355
 
                 full_path=None, debug=False):
356
 
        assert isinstance(controller_state, FakeControllerState)
357
 
        self.controller_state = controller_state
358
 
        if feature_flags is None:
359
 
            feature_flags = set()
360
 
        self.feature_flags = feature_flags
361
 
        self.version = version
362
 
        self.full_path = full_path
363
 
        self.debug = debug
364
 
        self.juju_timings = {}
365
 
        self.log = logging.getLogger('jujupy')
366
 
 
367
 
    def clone(self, full_path=None, version=None, debug=None,
368
 
              feature_flags=None):
369
 
        if version is None:
370
 
            version = self.version
371
 
        if full_path is None:
372
 
            full_path = self.full_path
373
 
        if debug is None:
374
 
            debug = self.debug
375
 
        if feature_flags is None:
376
 
            feature_flags = set(self.feature_flags)
377
 
        controller_state = self.controller_state
378
 
        return self.__class__(controller_state, feature_flags, version,
379
 
                              full_path, debug)
380
 
 
381
 
    def set_feature(self, feature, enabled):
382
 
        if enabled:
383
 
            self.feature_flags.add(feature)
384
 
        else:
385
 
            self.feature_flags.discard(feature)
386
 
 
387
 
    def is_feature_enabled(self, feature):
388
 
        if feature == 'jes':
389
 
            return True
390
 
        return bool(feature in self.feature_flags)
391
 
 
392
 
    @contextmanager
393
 
    def ignore_soft_deadline(self):
394
 
        yield
395
 
 
396
 
    @contextmanager
397
 
    def _check_timeouts(self):
398
 
        yield
399
 
 
400
 
    def deploy(self, model_state, charm_name, service_name=None, series=None):
401
 
        if service_name is None:
402
 
            service_name = charm_name.split(':')[-1].split('/')[-1]
403
 
        model_state.deploy(charm_name, service_name)
404
 
 
405
 
    def bootstrap(self, args):
406
 
        parser = ArgumentParser()
407
 
        parser.add_argument('controller_name')
408
 
        parser.add_argument('cloud_name_region')
409
 
        parser.add_argument('--constraints')
410
 
        parser.add_argument('--config')
411
 
        parser.add_argument('--default-model')
412
 
        parser.add_argument('--agent-version')
413
 
        parser.add_argument('--bootstrap-series')
414
 
        parser.add_argument('--upload-tools', action='store_true')
415
 
        parsed = parser.parse_args(args)
416
 
        with open(parsed.config) as config_file:
417
 
            config = yaml.safe_load(config_file)
418
 
        cloud_region = parsed.cloud_name_region.split('/', 1)
419
 
        cloud = cloud_region[0]
420
 
        # Although they are specified with specific arguments instead of as
421
 
        # config, these values are listed by model-config:
422
 
        # name, region, type (from cloud).
423
 
        config['type'] = cloud
424
 
        if len(cloud_region) > 1:
425
 
            config['region'] = cloud_region[1]
426
 
        config['name'] = parsed.default_model
427
 
        if parsed.bootstrap_series is not None:
428
 
            config['default-series'] = parsed.bootstrap_series
429
 
        self.controller_state.bootstrap(parsed.default_model, config,
430
 
                                        self.is_feature_enabled('jes'))
431
 
 
432
 
    def quickstart(self, model_name, config, bundle):
433
 
        default_model = self.controller_state.bootstrap(
434
 
            model_name, config, self.is_feature_enabled('jes'))
435
 
        default_model.deploy_bundle(bundle)
436
 
 
437
 
    def destroy_environment(self, model_name):
438
 
        try:
439
 
            state = self.controller_state.models[model_name]
440
 
        except KeyError:
441
 
            return 0
442
 
        state.destroy_environment()
443
 
        return 0
444
 
 
445
 
    def add_machines(self, model_state, args):
446
 
        if len(args) == 0:
447
 
            return model_state.add_machine()
448
 
        ssh_machines = [a[4:] for a in args if a.startswith('ssh:')]
449
 
        if len(ssh_machines) == len(args):
450
 
            return model_state.add_ssh_machines(ssh_machines)
451
 
        parser = ArgumentParser()
452
 
        parser.add_argument('host_placement', nargs='*')
453
 
        parser.add_argument('-n', type=int, dest='count', default='1')
454
 
        parsed = parser.parse_args(args)
455
 
        if len(parsed.host_placement) == 1:
456
 
            split = parsed.host_placement[0].split(':')
457
 
            if len(split) == 1:
458
 
                container_type = split[0]
459
 
                host = None
460
 
            else:
461
 
                container_type, host = split
462
 
            for x in range(parsed.count):
463
 
                model_state.add_container(container_type, host=host)
464
 
        else:
465
 
            for x in range(parsed.count):
466
 
                model_state.add_machine()
467
 
 
468
 
    def get_controller_model_name(self):
469
 
        return self.controller_state.controller_model.name
470
 
 
471
 
    def make_controller_dict(self, controller_name):
472
 
        controller_model = self.controller_state.controller_model
473
 
        server_id = list(controller_model.state_servers)[0]
474
 
        server_hostname = controller_model.machine_host_names[server_id]
475
 
        api_endpoint = '{}:23'.format(server_hostname)
476
 
        return {controller_name: {'details': {'api-endpoints': [
477
 
            api_endpoint]}}}
478
 
 
479
 
    def list_models(self):
480
 
        model_names = [state.name for state in
481
 
                       self.controller_state.models.values()]
482
 
        return {'models': [{'name': n} for n in model_names]}
483
 
 
484
 
    def list_users(self):
485
 
        user_names = [name for name in
486
 
                      self.controller_state.users.keys()]
487
 
        user_list = []
488
 
        for n in user_names:
489
 
            if n == 'admin':
490
 
                append_dict = {'access': 'superuser', 'user-name': n,
491
 
                               'display-name': n}
492
 
            else:
493
 
                access = self.controller_state.users[n]['access']
494
 
                append_dict = {
495
 
                    'access': access, 'user-name': n}
496
 
            user_list.append(append_dict)
497
 
        return user_list
498
 
 
499
 
    def show_user(self, user_name):
500
 
        if user_name is None:
501
 
            raise Exception("No user specified")
502
 
        if user_name == 'admin':
503
 
            user_status = {'access': 'superuser', 'user-name': user_name,
504
 
                           'display-name': user_name}
505
 
        else:
506
 
            user_status = {'user-name': user_name, 'display-name': ''}
507
 
        return user_status
508
 
 
509
 
    def get_users(self):
510
 
        share_names = self.controller_state.shares
511
 
        permissions = []
512
 
        for key, value in self.controller_state.users.iteritems():
513
 
            if key in share_names:
514
 
                permissions.append(value['permission'])
515
 
        share_list = {}
516
 
        for i, (share_name, permission) in enumerate(
517
 
                zip(share_names, permissions)):
518
 
            name = share_name + '@local'
519
 
            share_list[name] = {'display-name': share_name,
520
 
                                'access': permission}
521
 
            if name != 'admin@local':
522
 
                share_list[name].pop('display-name')
523
 
            else:
524
 
                share_list[name]['access'] = 'admin'
525
 
        return share_list
526
 
 
527
 
    def show_model(self):
528
 
        # To get data from the model we would need:
529
 
        # self.controller_state.current_model
530
 
        model_name = 'name'
531
 
        data = {
532
 
            'name': model_name,
533
 
            'owner': 'admin@local',
534
 
            'life': 'alive',
535
 
            'status': {'current': 'available', 'since': '15 minutes ago'},
536
 
            'users': self.get_users(),
537
 
            }
538
 
        return {model_name: data}
539
 
 
540
 
    def _log_command(self, command, args, model, level=logging.INFO):
541
 
        full_args = ['juju', command]
542
 
        if model is not None:
543
 
            full_args.extend(['-m', model])
544
 
        full_args.extend(args)
545
 
        self.log.log(level, ' '.join(full_args))
546
 
 
547
 
    def juju(self, command, args, used_feature_flags,
548
 
             juju_home, model=None, check=True, timeout=None, extra_env=None):
549
 
        if 'service' in command:
550
 
            raise Exception('Command names must not contain "service".')
551
 
        if isinstance(args, basestring):
552
 
            args = (args,)
553
 
        self._log_command(command, args, model)
554
 
        if model is not None:
555
 
            if ':' in model:
556
 
                model = model.split(':')[1]
557
 
            model_state = self.controller_state.models[model]
558
 
            if command == 'enable-ha':
559
 
                model_state.enable_ha()
560
 
            if ((command, args[:1]) == ('set-config', ('dummy-source',)) or
561
 
                    (command, args[:1]) == ('config', ('dummy-source',))):
562
 
                name, value = args[1].split('=')
563
 
                if name == 'token':
564
 
                    model_state.token = value
565
 
            if command == 'deploy':
566
 
                parser = ArgumentParser()
567
 
                parser.add_argument('charm_name')
568
 
                parser.add_argument('service_name', nargs='?')
569
 
                parser.add_argument('--to')
570
 
                parser.add_argument('--series')
571
 
                parsed = parser.parse_args(args)
572
 
                self.deploy(model_state, parsed.charm_name,
573
 
                            parsed.service_name, parsed.series)
574
 
            if command == 'remove-application':
575
 
                model_state.destroy_service(*args)
576
 
            if command == 'add-relation':
577
 
                if args[0] == 'dummy-source':
578
 
                    model_state.relations[args[1]] = {'source': [args[0]]}
579
 
            if command == 'expose':
580
 
                (service,) = args
581
 
                model_state.exposed.add(service)
582
 
            if command == 'unexpose':
583
 
                (service,) = args
584
 
                model_state.exposed.remove(service)
585
 
            if command == 'add-unit':
586
 
                (service,) = args
587
 
                model_state.add_unit(service)
588
 
            if command == 'remove-unit':
589
 
                (unit_id,) = args
590
 
                model_state.remove_unit(unit_id)
591
 
            if command == 'add-machine':
592
 
                return self.add_machines(model_state, args)
593
 
            if command == 'remove-machine':
594
 
                parser = ArgumentParser()
595
 
                parser.add_argument('machine_id')
596
 
                parser.add_argument('--force', action='store_true')
597
 
                parsed = parser.parse_args(args)
598
 
                machine_id = parsed.machine_id
599
 
                if '/' in machine_id:
600
 
                    model_state.remove_container(machine_id)
601
 
                else:
602
 
                    model_state.remove_machine(machine_id)
603
 
            if command == 'quickstart':
604
 
                parser = ArgumentParser()
605
 
                parser.add_argument('--constraints')
606
 
                parser.add_argument('--no-browser', action='store_true')
607
 
                parser.add_argument('bundle')
608
 
                parsed = parser.parse_args(args)
609
 
                # Released quickstart doesn't seem to provide the config via
610
 
                # the commandline.
611
 
                self.quickstart(model, {}, parsed.bundle)
612
 
        else:
613
 
            if command == 'bootstrap':
614
 
                self.bootstrap(args)
615
 
            if command == 'kill-controller':
616
 
                if self.controller_state.state == 'not-bootstrapped':
617
 
                    return
618
 
                model = args[0]
619
 
                model_state = self.controller_state.models[model]
620
 
                model_state.kill_controller()
621
 
            if command == 'destroy-model':
622
 
                if not self.is_feature_enabled('jes'):
623
 
                    raise JESNotSupported()
624
 
                model = args[0]
625
 
                model_state = self.controller_state.models[model]
626
 
                model_state.destroy_model()
627
 
            if command == 'add-model':
628
 
                if not self.is_feature_enabled('jes'):
629
 
                    raise JESNotSupported()
630
 
                parser = ArgumentParser()
631
 
                parser.add_argument('-c', '--controller')
632
 
                parser.add_argument('--config')
633
 
                parser.add_argument('model_name')
634
 
                parsed = parser.parse_args(args)
635
 
                self.controller_state.add_model(parsed.model_name)
636
 
            if command == 'revoke':
637
 
                user_name = args[2]
638
 
                permissions = args[3]
639
 
                per = self.controller_state.users[user_name]['permission']
640
 
                if per == permissions:
641
 
                    if permissions == 'read':
642
 
                        self.controller_state.shares.remove(user_name)
643
 
                        per = ''
644
 
                    else:
645
 
                        per = 'read'
646
 
            if command == 'grant':
647
 
                username = args[0]
648
 
                permission = args[1]
649
 
                self.controller_state.grant(username, permission)
650
 
            if command == 'remove-user':
651
 
                username = args[0]
652
 
                self.controller_state.users.pop(username)
653
 
                if username in self.controller_state.shares:
654
 
                    self.controller_state.shares.remove(username)
655
 
            if command == 'restore-backup':
656
 
                model_state.restore_backup()
657
 
 
658
 
    @contextmanager
659
 
    def juju_async(self, command, args, used_feature_flags,
660
 
                   juju_home, model=None, timeout=None):
661
 
        yield
662
 
        self.juju(command, args, used_feature_flags,
663
 
                  juju_home, model, timeout=timeout)
664
 
 
665
 
    @check_juju_output
666
 
    def get_juju_output(self, command, args, used_feature_flags, juju_home,
667
 
                        model=None, timeout=None, user_name=None,
668
 
                        merge_stderr=False):
669
 
        if 'service' in command:
670
 
            raise Exception('No service')
671
 
        self._log_command(command, args, model, logging.DEBUG)
672
 
        if model is not None:
673
 
            if ':' in model:
674
 
                model = model.split(':')[1]
675
 
            model_state = self.controller_state.models[model]
676
 
        from deploy_stack import GET_TOKEN_SCRIPT
677
 
        if (command, args) == ('ssh', ('dummy-sink/0', GET_TOKEN_SCRIPT)):
678
 
            return model_state.token
679
 
        if (command, args) == ('ssh', ('0', 'lsb_release', '-c')):
680
 
            return 'Codename:\t{}\n'.format(
681
 
                model_state.model_config['default-series'])
682
 
        if command in ('model-config', 'get-model-config'):
683
 
            return yaml.safe_dump(model_state.model_config)
684
 
        if command == 'show-controller':
685
 
            return yaml.safe_dump(self.make_controller_dict(args[0]))
686
 
        if command == 'list-models':
687
 
            return yaml.safe_dump(self.list_models())
688
 
        if command == 'list-users':
689
 
            return json.dumps(self.list_users())
690
 
        if command == 'show-model':
691
 
            return json.dumps(self.show_model())
692
 
        if command == 'show-user':
693
 
            return json.dumps(self.show_user(user_name))
694
 
        if command == 'add-user':
695
 
            permissions = 'read'
696
 
            if set(["--acl", "write"]).issubset(args):
697
 
                permissions = 'write'
698
 
            username = args[0]
699
 
            info_string = 'User "{}" added\n'.format(username)
700
 
            self.controller_state.add_user_perms(username, permissions)
701
 
            register_string = get_user_register_command_info(username)
702
 
            return info_string + register_string
703
 
        if command == 'show-status':
704
 
            status_dict = model_state.get_status_dict()
705
 
            # Parsing JSON is much faster than parsing YAML, and JSON is a
706
 
            # subset of YAML, so emit JSON.
707
 
            return json.dumps(status_dict)
708
 
        if command == 'create-backup':
709
 
            self.controller_state.require_controller('backup', model)
710
 
            return 'juju-backup-0.tar.gz'
711
 
        if command == 'ssh-keys':
712
 
            lines = ['Keys used in model: ' + model_state.name]
713
 
            if '--full' in args:
714
 
                lines.extend(model_state.ssh_keys)
715
 
            else:
716
 
                lines.extend(':fake:fingerprint: ({})'.format(
717
 
                    k.split(' ', 2)[-1]) for k in model_state.ssh_keys)
718
 
            return '\n'.join(lines)
719
 
        if command == 'add-ssh-key':
720
 
            return model_state.add_ssh_key(args)
721
 
        if command == 'remove-ssh-key':
722
 
            return model_state.remove_ssh_key(args)
723
 
        if command == 'import-ssh-key':
724
 
            return model_state.import_ssh_key(args)
725
 
        return ''
726
 
 
727
 
    def expect(self, command, args, used_feature_flags, juju_home, model=None,
728
 
               timeout=None, extra_env=None):
729
 
        if command == 'autoload-credentials':
730
 
            return AutoloadCredentials(self, juju_home, extra_env)
731
 
        else:
732
 
            return FakeExpectChild(self, juju_home, extra_env)
733
 
 
734
 
    def pause(self, seconds):
735
 
        pass
736
 
 
737
 
 
738
 
def get_user_register_command_info(username):
739
 
    code = get_user_register_token(username)
740
 
    return 'Please send this command to {}\n    juju register {}'.format(
741
 
        username, code)
742
 
 
743
 
 
744
 
def get_user_register_token(username):
745
 
    return b64encode(sha512(username).digest())
746
 
 
747
 
 
748
 
class FakeBackend2B7(FakeBackend):
749
 
 
750
 
    def juju(self, command, args, used_feature_flags,
751
 
             juju_home, model=None, check=True, timeout=None, extra_env=None):
752
 
        if model is not None:
753
 
            model_state = self.controller_state.models[model]
754
 
        if command == 'destroy-service':
755
 
            model_state.destroy_service(*args)
756
 
        if command == 'remove-service':
757
 
            model_state.destroy_service(*args)
758
 
        return super(FakeBackend2B7).juju(command, args, used_feature_flags,
759
 
                                          juju_home, model, check, timeout,
760
 
                                          extra_env)
761
 
 
762
 
 
763
 
class FakeBackendOptionalJES(FakeBackend):
764
 
 
765
 
    def is_feature_enabled(self, feature):
766
 
        return bool(feature in self.feature_flags)
767
 
 
768
 
 
769
 
def fake_juju_client(env=None, full_path=None, debug=False, version='2.0.0',
770
 
                     _backend=None, cls=EnvJujuClient):
771
 
    if env is None:
772
 
        env = JujuData('name', {
773
 
            'type': 'foo',
774
 
            'default-series': 'angsty',
775
 
            'region': 'bar',
776
 
            }, juju_home='foo')
777
 
        env.credentials = {'credentials': {'foo': {'creds': {}}}}
778
 
    juju_home = env.juju_home
779
 
    if juju_home is None:
780
 
        juju_home = 'foo'
781
 
    if _backend is None:
782
 
        backend_state = FakeControllerState()
783
 
        _backend = FakeBackend(
784
 
            backend_state, version=version, full_path=full_path,
785
 
            debug=debug)
786
 
        _backend.set_feature('jes', True)
787
 
    client = cls(
788
 
        env, version, full_path, juju_home, debug, _backend=_backend)
789
 
    client.bootstrap_replaces = {}
790
 
    return client
791
 
 
792
 
 
793
 
def fake_juju_client_optional_jes(env=None, full_path=None, debug=False,
794
 
                                  jes_enabled=True, version='2.0.0',
795
 
                                  _backend=None):
796
 
    if _backend is None:
797
 
        backend_state = FakeControllerState()
798
 
        _backend = FakeBackendOptionalJES(
799
 
            backend_state, version=version, full_path=full_path,
800
 
            debug=debug)
801
 
        _backend.set_feature('jes', jes_enabled)
802
 
    client = fake_juju_client(env, full_path, debug, version, _backend,
803
 
                              cls=FakeJujuClientOptionalJES)
804
 
    client.used_feature_flags = frozenset(['address-allocation', 'jes'])
805
 
    return client
806
 
 
807
 
 
808
 
class FakeJujuClientOptionalJES(EnvJujuClient):
809
 
 
810
 
    def get_controller_model_name(self):
811
 
        return self._backend.controller_state.controller_model.name