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

« back to all changes in this revision

Viewing changes to jujupy.py

Handle LoggedException in quickstart_deploy, assess_bootstrap.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
from __future__ import print_function
2
2
 
3
 
from collections import (
4
 
    defaultdict,
5
 
    namedtuple,
6
 
)
 
3
from collections import defaultdict
7
4
from contextlib import (
8
5
    contextmanager,
9
6
    nested,
10
7
)
11
 
from copy import deepcopy
12
8
from cStringIO import StringIO
13
 
from datetime import datetime
 
9
from datetime import timedelta
14
10
import errno
15
11
from itertools import chain
16
 
import json
17
12
import logging
18
13
import os
19
 
import pexpect
20
14
import re
21
15
from shutil import rmtree
22
16
import subprocess
23
17
import sys
24
 
from tempfile import NamedTemporaryFile
25
18
import time
26
19
 
27
20
import yaml
37
30
    ensure_deleted,
38
31
    ensure_dir,
39
32
    is_ipv6_address,
40
 
    JujuResourceTimeout,
41
33
    pause,
42
 
    quote,
43
 
    qualified_model_name,
44
34
    scoped_environ,
45
 
    split_address_port,
46
35
    temp_dir,
47
 
    unqualified_model_name,
48
36
    until_timeout,
49
37
)
50
38
 
51
39
 
52
40
__metaclass__ = type
53
41
 
54
 
AGENTS_READY = set(['started', 'idle'])
 
42
 
55
43
WIN_JUJU_CMD = os.path.join('\\', 'Progra~2', 'Juju', 'juju.exe')
56
44
 
57
45
JUJU_DEV_FEATURE_FLAGS = 'JUJU_DEV_FEATURE_FLAGS'
58
 
CONTROLLER = 'controller'
59
 
KILL_CONTROLLER = 'kill-controller'
60
 
SYSTEM = 'system'
61
 
 
62
 
KVM_MACHINE = 'kvm'
63
 
LXC_MACHINE = 'lxc'
64
 
LXD_MACHINE = 'lxd'
65
 
 
66
 
_DEFAULT_BUNDLE_TIMEOUT = 3600
67
 
 
68
 
_jes_cmds = {KILL_CONTROLLER: {
 
46
DEFAULT_JES_COMMAND_2x = 'controller'
 
47
DEFAULT_JES_COMMAND_1x = 'destroy-controller'
 
48
OPTIONAL_JES_COMMAND = 'system'
 
49
 
 
50
_jes_cmds = {DEFAULT_JES_COMMAND_1x: {
69
51
    'create': 'create-environment',
70
 
    'kill': KILL_CONTROLLER,
71
 
}}
72
 
for super_cmd in [SYSTEM, CONTROLLER]:
 
52
    'kill': DEFAULT_JES_COMMAND_1x,
 
53
    }}
 
54
for super_cmd in [OPTIONAL_JES_COMMAND, DEFAULT_JES_COMMAND_2x]:
73
55
    _jes_cmds[super_cmd] = {
74
56
        'create': '{} create-environment'.format(super_cmd),
75
57
        'kill': '{} kill'.format(super_cmd),
76
 
    }
 
58
        }
77
59
 
78
60
log = logging.getLogger("jujupy")
79
61
 
80
62
 
81
 
class IncompatibleConfigClass(Exception):
82
 
    """Raised when a client is initialised with the wrong config class."""
83
 
 
84
 
 
85
 
class SoftDeadlineExceeded(Exception):
86
 
    """Raised when an overall client operation takes too long."""
87
 
 
88
 
 
89
63
def get_timeout_path():
90
64
    import timeout
91
65
    return os.path.abspath(timeout.__file__)
98
72
    return (sys.executable, timeout_path, '%.2f' % duration, '--')
99
73
 
100
74
 
101
 
def get_teardown_timeout(client):
102
 
    """Return the timeout need byt the client to teardown resources."""
103
 
    if client.env.config['type'] == 'azure':
104
 
        return 1800
105
 
    else:
106
 
        return 600
107
 
 
108
 
 
109
75
def parse_new_state_server_from_error(error):
110
76
    err_str = str(error)
111
77
    output = getattr(error, 'output', None)
126
92
        self.state = state
127
93
 
128
94
 
129
 
class BootstrapMismatch(Exception):
130
 
 
131
 
    def __init__(self, arg_name, arg_val, env_name, env_val):
132
 
        super(BootstrapMismatch, self).__init__(
133
 
            '--{} {} does not match {}: {}'.format(
134
 
                arg_name, arg_val, env_name, env_val))
135
 
 
136
 
 
137
 
class UpgradeMongoNotSupported(Exception):
138
 
 
139
 
    def __init__(self):
140
 
        super(UpgradeMongoNotSupported, self).__init__(
141
 
            'This client does not support upgrade-mongo')
142
 
 
143
 
 
144
95
class JESNotSupported(Exception):
145
96
 
146
97
    def __init__(self):
155
106
            'This client does not need to enable JES')
156
107
 
157
108
 
158
 
Machine = namedtuple('Machine', ['machine_id', 'info'])
159
 
 
160
 
 
161
109
def yaml_loads(yaml_str):
162
110
    return yaml.safe_load(StringIO(yaml_str))
163
111
 
164
112
 
165
 
def coalesce_agent_status(agent_item):
166
 
    """Return the machine agent-state or the unit agent-status."""
167
 
    state = agent_item.get('agent-state')
168
 
    if state is None and agent_item.get('agent-status') is not None:
169
 
        state = agent_item.get('agent-status').get('current')
170
 
    if state is None and agent_item.get('juju-status') is not None:
171
 
        state = agent_item.get('juju-status').get('current')
172
 
    if state is None:
173
 
        state = 'no-agent'
174
 
    return state
 
113
def make_client(juju_path, debug, env_name, temp_env_name):
 
114
    env = SimpleEnvironment.from_config(env_name)
 
115
    if temp_env_name is not None:
 
116
        env.environment = temp_env_name
 
117
        env.config['name'] = temp_env_name
 
118
    return EnvJujuClient.by_version(env, juju_path, debug)
175
119
 
176
120
 
177
121
class CannotConnectEnv(subprocess.CalledProcessError):
207
151
    _fmt = 'Workloads not ready in {env}.'
208
152
 
209
153
 
210
 
@contextmanager
211
 
def temp_yaml_file(yaml_dict):
212
 
    temp_file = NamedTemporaryFile(suffix='.yaml', delete=False)
213
 
    try:
214
 
        with temp_file:
215
 
            yaml.safe_dump(yaml_dict, temp_file)
216
 
        yield temp_file.name
217
 
    finally:
218
 
        os.unlink(temp_file.name)
219
 
 
220
 
 
221
 
class SimpleEnvironment:
222
 
    """Represents a model in a JUJU_HOME directory for juju 1."""
223
 
 
224
 
    def __init__(self, environment, config=None, juju_home=None,
225
 
                 controller=None):
226
 
        """Constructor.
227
 
 
228
 
        :param environment: Name of the environment.
229
 
        :param config: Dictionary with configuration options, default is None.
230
 
        :param juju_home: Path to JUJU_HOME directory, default is None.
231
 
        :param controller: Controller instance-- this model's controller.
232
 
            If not given or None a new instance is created."""
233
 
        self.user_name = None
234
 
        if controller is None:
235
 
            controller = Controller(environment)
236
 
        self.controller = controller
237
 
        self.environment = environment
238
 
        self.config = config
239
 
        self.juju_home = juju_home
240
 
        if self.config is not None:
241
 
            self.local = bool(self.config.get('type') == 'local')
242
 
            self.kvm = (
243
 
                self.local and bool(self.config.get('container') == 'kvm'))
244
 
            self.maas = bool(self.config.get('type') == 'maas')
245
 
            self.joyent = bool(self.config.get('type') == 'joyent')
246
 
        else:
247
 
            self.local = False
248
 
            self.kvm = False
249
 
            self.maas = False
250
 
            self.joyent = False
251
 
 
252
 
    def clone(self, model_name=None):
253
 
        config = deepcopy(self.config)
254
 
        if model_name is None:
255
 
            model_name = self.environment
256
 
        else:
257
 
            config['name'] = unqualified_model_name(model_name)
258
 
        result = self.__class__(model_name, config, self.juju_home,
259
 
                                self.controller)
260
 
        result.local = self.local
261
 
        result.kvm = self.kvm
262
 
        result.maas = self.maas
263
 
        result.joyent = self.joyent
264
 
        return result
265
 
 
266
 
    def __eq__(self, other):
267
 
        if type(self) != type(other):
268
 
            return False
269
 
        if self.environment != other.environment:
270
 
            return False
271
 
        if self.config != other.config:
272
 
            return False
273
 
        if self.local != other.local:
274
 
            return False
275
 
        if self.maas != other.maas:
276
 
            return False
277
 
        return True
278
 
 
279
 
    def __ne__(self, other):
280
 
        return not self == other
281
 
 
282
 
    def set_model_name(self, model_name, set_controller=True):
283
 
        if set_controller:
284
 
            self.controller.name = model_name
285
 
        self.environment = model_name
286
 
        self.config['name'] = unqualified_model_name(model_name)
287
 
 
288
 
    @classmethod
289
 
    def from_config(cls, name):
290
 
        """Create an environment from the configuation file.
291
 
 
292
 
        :param name: Name of the environment to get the configuration from."""
293
 
        return cls._from_config(name)
294
 
 
295
 
    @classmethod
296
 
    def _from_config(cls, name):
297
 
        config, selected = get_selected_environment(name)
298
 
        if name is None:
299
 
            name = selected
300
 
        return cls(name, config)
301
 
 
302
 
    def needs_sudo(self):
303
 
        return self.local
304
 
 
305
 
    @contextmanager
306
 
    def make_jes_home(self, juju_home, dir_name, new_config):
307
 
        home_path = jes_home_path(juju_home, dir_name)
308
 
        if os.path.exists(home_path):
309
 
            rmtree(home_path)
310
 
        os.makedirs(home_path)
311
 
        self.dump_yaml(home_path, new_config)
312
 
        yield home_path
313
 
 
314
 
    def get_cloud_credentials(self):
315
 
        """Return the credentials for this model's cloud.
316
 
 
317
 
        This implementation returns config variables in addition to
318
 
        credentials.
319
 
        """
320
 
        return self.config
321
 
 
322
 
    def dump_yaml(self, path, config):
323
 
        dump_environments_yaml(path, config)
324
 
 
325
 
 
326
 
class JujuData(SimpleEnvironment):
327
 
    """Represents a model in a JUJU_DATA directory for juju 2."""
328
 
 
329
 
    def __init__(self, environment, config=None, juju_home=None,
330
 
                 controller=None):
331
 
        """Constructor.
332
 
 
333
 
        This extends SimpleEnvironment's constructor.
334
 
 
335
 
        :param environment: Name of the environment.
336
 
        :param config: Dictionary with configuration options; default is None.
337
 
        :param juju_home: Path to JUJU_DATA directory. If None (the default),
338
 
            the home directory is autodetected.
339
 
        :param controller: Controller instance-- this model's controller.
340
 
            If not given or None, a new instance is created.
341
 
        """
342
 
        if juju_home is None:
343
 
            juju_home = get_juju_home()
344
 
        super(JujuData, self).__init__(environment, config, juju_home,
345
 
                                       controller)
346
 
        self.credentials = {}
347
 
        self.clouds = {}
348
 
 
349
 
    def clone(self, model_name=None):
350
 
        result = super(JujuData, self).clone(model_name)
351
 
        result.credentials = deepcopy(self.credentials)
352
 
        result.clouds = deepcopy(self.clouds)
353
 
        return result
354
 
 
355
 
    @classmethod
356
 
    def from_env(cls, env):
357
 
        juju_data = cls(env.environment, env.config, env.juju_home)
358
 
        juju_data.load_yaml()
359
 
        return juju_data
360
 
 
361
 
    def load_yaml(self):
362
 
        try:
363
 
            with open(os.path.join(self.juju_home, 'credentials.yaml')) as f:
364
 
                self.credentials = yaml.safe_load(f)
365
 
        except IOError as e:
366
 
            if e.errno != errno.ENOENT:
367
 
                raise RuntimeError(
368
 
                    'Failed to read credentials file: {}'.format(str(e)))
369
 
            self.credentials = {}
370
 
        try:
371
 
            with open(os.path.join(self.juju_home, 'clouds.yaml')) as f:
372
 
                self.clouds = yaml.safe_load(f)
373
 
        except IOError as e:
374
 
            if e.errno != errno.ENOENT:
375
 
                raise RuntimeError(
376
 
                    'Failed to read clouds file: {}'.format(str(e)))
377
 
            # Default to an empty clouds file.
378
 
            self.clouds = {'clouds': {}}
379
 
 
380
 
    @classmethod
381
 
    def from_config(cls, name):
382
 
        """Create a model from the three configuration files."""
383
 
        juju_data = cls._from_config(name)
384
 
        juju_data.load_yaml()
385
 
        return juju_data
386
 
 
387
 
    def dump_yaml(self, path, config):
388
 
        """Dump the configuration files to the specified path.
389
 
 
390
 
        config is unused, but is accepted for compatibility with
391
 
        SimpleEnvironment and make_jes_home().
392
 
        """
393
 
        with open(os.path.join(path, 'credentials.yaml'), 'w') as f:
394
 
            yaml.safe_dump(self.credentials, f)
395
 
        with open(os.path.join(path, 'clouds.yaml'), 'w') as f:
396
 
            yaml.safe_dump(self.clouds, f)
397
 
 
398
 
    def find_endpoint_cloud(self, cloud_type, endpoint):
399
 
        for cloud, cloud_config in self.clouds['clouds'].items():
400
 
            if cloud_config['type'] != cloud_type:
401
 
                continue
402
 
            if cloud_config['endpoint'] == endpoint:
403
 
                return cloud
404
 
        raise LookupError('No such endpoint: {}'.format(endpoint))
405
 
 
406
 
    def get_cloud(self):
407
 
        provider = self.config['type']
408
 
        # Separate cloud recommended by: Juju Cloud / Credentials / BootStrap /
409
 
        # Model CLI specification
410
 
        if provider == 'ec2' and self.config['region'] == 'cn-north-1':
411
 
            return 'aws-china'
412
 
        if provider not in ('maas', 'openstack'):
413
 
            return {
414
 
                'ec2': 'aws',
415
 
                'gce': 'google',
416
 
            }.get(provider, provider)
417
 
        if provider == 'maas':
418
 
            endpoint = self.config['maas-server']
419
 
        elif provider == 'openstack':
420
 
            endpoint = self.config['auth-url']
421
 
        return self.find_endpoint_cloud(provider, endpoint)
422
 
 
423
 
    def get_region(self):
424
 
        provider = self.config['type']
425
 
        if provider == 'azure':
426
 
            if 'tenant-id' not in self.config:
427
 
                return self.config['location'].replace(' ', '').lower()
428
 
            return self.config['location']
429
 
        elif provider == 'joyent':
430
 
            matcher = re.compile('https://(.*).api.joyentcloud.com')
431
 
            return matcher.match(self.config['sdc-url']).group(1)
432
 
        elif provider == 'lxd':
433
 
            return 'localhost'
434
 
        elif provider == 'manual':
435
 
            return self.config['bootstrap-host']
436
 
        elif provider in ('maas', 'manual'):
437
 
            return None
438
 
        else:
439
 
            return self.config['region']
440
 
 
441
 
    def get_cloud_credentials(self):
442
 
        """Return the credentials for this model's cloud."""
443
 
        cloud_name = self.get_cloud()
444
 
        cloud = self.credentials['credentials'][cloud_name]
445
 
        (credentials,) = cloud.values()
446
 
        return credentials
447
 
 
448
 
 
449
 
class Status:
450
 
 
451
 
    def __init__(self, status, status_text):
452
 
        self.status = status
453
 
        self.status_text = status_text
454
 
 
455
 
    @classmethod
456
 
    def from_text(cls, text):
457
 
        try:
458
 
            # Parsing as JSON is much faster than parsing as YAML, so try
459
 
            # parsing as JSON first and fall back to YAML.
460
 
            status_yaml = json.loads(text)
461
 
        except ValueError:
462
 
            status_yaml = yaml_loads(text)
463
 
        return cls(status_yaml, text)
464
 
 
465
 
    def get_applications(self):
466
 
        return self.status.get('applications', {})
467
 
 
468
 
    def iter_machines(self, containers=False, machines=True):
469
 
        for machine_name, machine in sorted(self.status['machines'].items()):
470
 
            if machines:
471
 
                yield machine_name, machine
472
 
            if containers:
473
 
                for contained, unit in machine.get('containers', {}).items():
474
 
                    yield contained, unit
475
 
 
476
 
    def iter_new_machines(self, old_status):
477
 
        for machine, data in self.iter_machines():
478
 
            if machine in old_status.status['machines']:
479
 
                continue
480
 
            yield machine, data
481
 
 
482
 
    def iter_units(self):
483
 
        for service_name, service in sorted(self.get_applications().items()):
484
 
            for unit_name, unit in sorted(service.get('units', {}).items()):
485
 
                yield unit_name, unit
486
 
                subordinates = unit.get('subordinates', ())
487
 
                for sub_name in sorted(subordinates):
488
 
                    yield sub_name, subordinates[sub_name]
489
 
 
490
 
    def agent_items(self):
491
 
        for machine_name, machine in self.iter_machines(containers=True):
492
 
            yield machine_name, machine
493
 
        for unit_name, unit in self.iter_units():
494
 
            yield unit_name, unit
495
 
 
496
 
    def agent_states(self):
497
 
        """Map agent states to the units and machines in those states."""
498
 
        states = defaultdict(list)
499
 
        for item_name, item in self.agent_items():
500
 
            states[coalesce_agent_status(item)].append(item_name)
501
 
        return states
502
 
 
503
 
    def check_agents_started(self, environment_name=None):
504
 
        """Check whether all agents are in the 'started' state.
505
 
 
506
 
        If not, return agent_states output.  If so, return None.
507
 
        If an error is encountered for an agent, raise ErroredUnit
508
 
        """
509
 
        bad_state_info = re.compile(
510
 
            '(.*error|^(cannot set up groups|cannot run instance)).*')
511
 
        for item_name, item in self.agent_items():
512
 
            state_info = item.get('agent-state-info', '')
513
 
            if bad_state_info.match(state_info):
514
 
                raise ErroredUnit(item_name, state_info)
515
 
        states = self.agent_states()
516
 
        if set(states.keys()).issubset(AGENTS_READY):
517
 
            return None
518
 
        for state, entries in states.items():
519
 
            if 'error' in state:
520
 
                # sometimes the state may be hidden in juju status message
521
 
                juju_status = dict(
522
 
                    self.agent_items())[entries[0]].get('juju-status')
523
 
                if juju_status:
524
 
                    juju_status_msg = juju_status.get('message')
525
 
                    if juju_status_msg:
526
 
                        state = juju_status_msg
527
 
                raise ErroredUnit(entries[0], state)
528
 
        return states
529
 
 
530
 
    def get_service_count(self):
531
 
        return len(self.get_applications())
532
 
 
533
 
    def get_service_unit_count(self, service):
534
 
        return len(
535
 
            self.get_applications().get(service, {}).get('units', {}))
536
 
 
537
 
    def get_agent_versions(self):
538
 
        versions = defaultdict(set)
539
 
        for item_name, item in self.agent_items():
540
 
            if item.get('juju-status', None):
541
 
                version = item['juju-status'].get('version', 'unknown')
542
 
                versions[version].add(item_name)
543
 
            else:
544
 
                versions[item.get('agent-version', 'unknown')].add(item_name)
545
 
        return versions
546
 
 
547
 
    def get_instance_id(self, machine_id):
548
 
        return self.status['machines'][machine_id]['instance-id']
549
 
 
550
 
    def get_unit(self, unit_name):
551
 
        """Return metadata about a unit."""
552
 
        for service in sorted(self.get_applications().values()):
553
 
            if unit_name in service.get('units', {}):
554
 
                return service['units'][unit_name]
555
 
        raise KeyError(unit_name)
556
 
 
557
 
    def service_subordinate_units(self, service_name):
558
 
        """Return subordinate metadata for a service_name."""
559
 
        services = self.get_applications()
560
 
        if service_name in services:
561
 
            for unit in sorted(services[service_name].get(
562
 
                    'units', {}).values()):
563
 
                for sub_name, sub in unit.get('subordinates', {}).items():
564
 
                    yield sub_name, sub
565
 
 
566
 
    def get_open_ports(self, unit_name):
567
 
        """List the open ports for the specified unit.
568
 
 
569
 
        If no ports are listed for the unit, the empty list is returned.
570
 
        """
571
 
        return self.get_unit(unit_name).get('open-ports', [])
572
 
 
573
 
 
574
 
class ServiceStatus(Status):
575
 
 
576
 
    def get_applications(self):
577
 
        return self.status.get('services', {})
578
 
 
579
 
 
580
 
class Juju2Backend:
581
 
    """A Juju backend referring to a specific juju 2 binary.
582
 
 
583
 
    Uses -m to specify models, uses JUJU_DATA to specify home directory.
584
 
    """
585
 
 
586
 
    def __init__(self, full_path, version, feature_flags, debug,
587
 
                 soft_deadline=None):
588
 
        self._version = version
589
 
        self._full_path = full_path
590
 
        self.feature_flags = feature_flags
591
 
        self.debug = debug
592
 
        self._timeout_path = get_timeout_path()
593
 
        self.juju_timings = {}
594
 
        self.soft_deadline = soft_deadline
595
 
        self._ignore_soft_deadline = False
596
 
 
597
 
    def _now(self):
598
 
        return datetime.utcnow()
599
 
 
600
 
    @contextmanager
601
 
    def _check_timeouts(self):
602
 
        # If an exception occurred, we don't want to replace it with
603
 
        # SoftDeadlineExceeded.
604
 
        yield
605
 
        if self.soft_deadline is None or self._ignore_soft_deadline:
606
 
            return
607
 
        if self._now() > self.soft_deadline:
608
 
            raise SoftDeadlineExceeded('Operation exceeded deadline.')
609
 
 
610
 
    @contextmanager
611
 
    def ignore_soft_deadline(self):
612
 
        """Ignore the client deadline.  For cleanup code."""
613
 
        old_val = self._ignore_soft_deadline
614
 
        self._ignore_soft_deadline = True
615
 
        try:
616
 
            yield
617
 
        finally:
618
 
            self._ignore_soft_deadline = old_val
619
 
 
620
 
    def clone(self, full_path, version, debug, feature_flags):
621
 
        if version is None:
622
 
            version = self.version
623
 
        if full_path is None:
624
 
            full_path = self.full_path
625
 
        if debug is None:
626
 
            debug = self.debug
627
 
        result = self.__class__(full_path, version, feature_flags, debug,
628
 
                                self.soft_deadline)
629
 
        return result
630
 
 
631
 
    @property
632
 
    def version(self):
633
 
        return self._version
634
 
 
635
 
    @property
636
 
    def full_path(self):
637
 
        return self._full_path
638
 
 
639
 
    @property
640
 
    def juju_name(self):
641
 
        return os.path.basename(self._full_path)
642
 
 
643
 
    def _get_attr_tuple(self):
644
 
        return (self._version, self._full_path, self.feature_flags,
645
 
                self.debug, self.juju_timings)
646
 
 
647
 
    def __eq__(self, other):
648
 
        if type(self) != type(other):
649
 
            return False
650
 
        return self._get_attr_tuple() == other._get_attr_tuple()
651
 
 
652
 
    def shell_environ(self, used_feature_flags, juju_home):
653
 
        """Generate a suitable shell environment.
654
 
 
655
 
        Juju's directory must be in the PATH to support plugins.
656
 
        """
657
 
        env = dict(os.environ)
658
 
        if self.full_path is not None:
659
 
            env['PATH'] = '{}{}{}'.format(os.path.dirname(self.full_path),
660
 
                                          os.pathsep, env['PATH'])
661
 
        flags = self.feature_flags.intersection(used_feature_flags)
662
 
        if flags:
663
 
            env[JUJU_DEV_FEATURE_FLAGS] = ','.join(sorted(flags))
664
 
        env['JUJU_DATA'] = juju_home
665
 
        return env
666
 
 
667
 
    def full_args(self, command, args, model, timeout):
668
 
        if model is not None:
669
 
            e_arg = ('-m', model)
670
 
        else:
671
 
            e_arg = ()
672
 
        if timeout is None:
673
 
            prefix = ()
674
 
        else:
675
 
            prefix = get_timeout_prefix(timeout, self._timeout_path)
676
 
        logging = '--debug' if self.debug else '--show-log'
677
 
 
678
 
        # If args is a string, make it a tuple. This makes writing commands
679
 
        # with one argument a bit nicer.
680
 
        if isinstance(args, basestring):
681
 
            args = (args,)
682
 
        # we split the command here so that the caller can control where the -m
683
 
        # model flag goes.  Everything in the command string is put before the
684
 
        # -m flag.
685
 
        command = command.split()
686
 
        return (prefix + (self.juju_name, logging,) + tuple(command) + e_arg +
687
 
                args)
688
 
 
689
 
    def juju(self, command, args, used_feature_flags,
690
 
             juju_home, model=None, check=True, timeout=None, extra_env=None):
691
 
        """Run a command under juju for the current environment."""
692
 
        args = self.full_args(command, args, model, timeout)
693
 
        log.info(' '.join(args))
694
 
        env = self.shell_environ(used_feature_flags, juju_home)
695
 
        if extra_env is not None:
696
 
            env.update(extra_env)
697
 
        if check:
698
 
            call_func = subprocess.check_call
699
 
        else:
700
 
            call_func = subprocess.call
701
 
        start_time = time.time()
702
 
        # Mutate os.environ instead of supplying env parameter so Windows can
703
 
        # search env['PATH']
704
 
        with scoped_environ(env):
705
 
            with self._check_timeouts():
706
 
                rval = call_func(args)
707
 
        self.juju_timings.setdefault(args, []).append(
708
 
            (time.time() - start_time))
709
 
        return rval
710
 
 
711
 
    def expect(self, command, args, used_feature_flags, juju_home, model=None,
712
 
               timeout=None, extra_env=None):
713
 
        args = self.full_args(command, args, model, timeout)
714
 
        log.info(' '.join(args))
715
 
        env = self.shell_environ(used_feature_flags, juju_home)
716
 
        if extra_env is not None:
717
 
            env.update(extra_env)
718
 
        # pexpect.spawn expects a string. This is better than trying to extract
719
 
        # command + args from the returned tuple (as there could be an intial
720
 
        # timing command tacked on).
721
 
        command_string = ' '.join(quote(a) for a in args)
722
 
        with scoped_environ(env):
723
 
            return pexpect.spawn(command_string)
724
 
 
725
 
    @contextmanager
726
 
    def juju_async(self, command, args, used_feature_flags,
727
 
                   juju_home, model=None, timeout=None):
728
 
        full_args = self.full_args(command, args, model, timeout)
729
 
        log.info(' '.join(args))
730
 
        env = self.shell_environ(used_feature_flags, juju_home)
731
 
        # Mutate os.environ instead of supplying env parameter so Windows can
732
 
        # search env['PATH']
733
 
        with scoped_environ(env):
734
 
            with self._check_timeouts():
735
 
                proc = subprocess.Popen(full_args)
736
 
        yield proc
737
 
        retcode = proc.wait()
738
 
        if retcode != 0:
739
 
            raise subprocess.CalledProcessError(retcode, full_args)
740
 
 
741
 
    def get_juju_output(self, command, args, used_feature_flags, juju_home,
742
 
                        model=None, timeout=None, user_name=None,
743
 
                        merge_stderr=False):
744
 
        args = self.full_args(command, args, model, timeout)
745
 
        env = self.shell_environ(used_feature_flags, juju_home)
746
 
        log.debug(args)
747
 
        # Mutate os.environ instead of supplying env parameter so
748
 
        # Windows can search env['PATH']
749
 
        with scoped_environ(env):
750
 
            proc = subprocess.Popen(
751
 
                args, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
752
 
                stderr=subprocess.STDOUT if merge_stderr else subprocess.PIPE)
753
 
            with self._check_timeouts():
754
 
                sub_output, sub_error = proc.communicate()
755
 
            log.debug(sub_output)
756
 
            if proc.returncode != 0:
757
 
                log.debug(sub_error)
758
 
                e = subprocess.CalledProcessError(
759
 
                    proc.returncode, args, sub_output)
760
 
                e.stderr = sub_error
761
 
                if sub_error and (
762
 
                    'Unable to connect to environment' in sub_error or
763
 
                        'MissingOrIncorrectVersionHeader' in sub_error or
764
 
                        '307: Temporary Redirect' in sub_error):
765
 
                    raise CannotConnectEnv(e)
766
 
                raise e
767
 
        return sub_output
768
 
 
769
 
    def pause(self, seconds):
770
 
        pause(seconds)
771
 
 
772
 
 
773
 
class Juju2A2Backend(Juju2Backend):
774
 
    """Backend for 2A2.
775
 
 
776
 
    Uses -m to specify models, uses JUJU_HOME and JUJU_DATA to specify home
777
 
    directory.
778
 
    """
779
 
 
780
 
    def shell_environ(self, used_feature_flags, juju_home):
781
 
        """Generate a suitable shell environment.
782
 
 
783
 
        For 2.0-alpha2 set both JUJU_HOME and JUJU_DATA.
784
 
        """
785
 
        env = super(Juju2A2Backend, self).shell_environ(used_feature_flags,
786
 
                                                        juju_home)
787
 
        env['JUJU_HOME'] = juju_home
788
 
        return env
789
 
 
790
 
 
791
 
class Juju1XBackend(Juju2A2Backend):
792
 
    """Backend for Juju 1.x - 2A1.
793
 
 
794
 
    Uses -e to specify models ("environments", uses JUJU_HOME to specify home
795
 
    directory.
796
 
    """
797
 
 
798
 
    def shell_environ(self, used_feature_flags, juju_home):
799
 
        """Generate a suitable shell environment.
800
 
 
801
 
        For 2.0-alpha1 and earlier set only JUJU_HOME and not JUJU_DATA.
802
 
        """
803
 
        env = super(Juju1XBackend, self).shell_environ(used_feature_flags,
804
 
                                                       juju_home)
805
 
        env['JUJU_HOME'] = juju_home
806
 
        del env['JUJU_DATA']
807
 
        return env
808
 
 
809
 
    def full_args(self, command, args, model, timeout):
810
 
        if model is None:
811
 
            e_arg = ()
812
 
        else:
813
 
            # In 1.x terminology, "model" is "environment".
814
 
            e_arg = ('-e', model)
815
 
        if timeout is None:
816
 
            prefix = ()
817
 
        else:
818
 
            prefix = get_timeout_prefix(timeout, self._timeout_path)
819
 
        logging = '--debug' if self.debug else '--show-log'
820
 
 
821
 
        # If args is a string, make it a tuple. This makes writing commands
822
 
        # with one argument a bit nicer.
823
 
        if isinstance(args, basestring):
824
 
            args = (args,)
825
 
        # we split the command here so that the caller can control where the -e
826
 
        # <env> flag goes.  Everything in the command string is put before the
827
 
        # -e flag.
828
 
        command = command.split()
829
 
        return (prefix + (self.juju_name, logging,) + tuple(command) + e_arg +
830
 
                args)
831
 
 
832
 
 
833
 
def get_client_class(version):
834
 
    if version.startswith('1.16'):
835
 
        raise Exception('Unsupported juju: %s' % version)
836
 
    elif re.match('^1\.22[.-]', version):
837
 
        client_class = EnvJujuClient22
838
 
    elif re.match('^1\.24[.-]', version):
839
 
        client_class = EnvJujuClient24
840
 
    elif re.match('^1\.25[.-]', version):
841
 
        client_class = EnvJujuClient25
842
 
    elif re.match('^1\.26[.-]', version):
843
 
        client_class = EnvJujuClient26
844
 
    elif re.match('^1\.', version):
845
 
        client_class = EnvJujuClient1X
846
 
    # Ensure alpha/beta number matches precisely
847
 
    elif re.match('^2\.0-alpha1([^\d]|$)', version):
848
 
        client_class = EnvJujuClient2A1
849
 
    elif re.match('^2\.0-alpha2([^\d]|$)', version):
850
 
        client_class = EnvJujuClient2A2
851
 
    elif re.match('^2\.0-(alpha3|beta[12])([^\d]|$)', version):
852
 
        client_class = EnvJujuClient2B2
853
 
    elif re.match('^2\.0-(beta[3-6])([^\d]|$)', version):
854
 
        client_class = EnvJujuClient2B3
855
 
    elif re.match('^2\.0-(beta7)([^\d]|$)', version):
856
 
        client_class = EnvJujuClient2B7
857
 
    elif re.match('^2\.0-beta8([^\d]|$)', version):
858
 
        client_class = EnvJujuClient2B8
859
 
    # between beta 9-14
860
 
    elif re.match('^2\.0-beta(9|1[0-4])([^\d]|$)', version):
861
 
        client_class = EnvJujuClient2B9
862
 
    else:
863
 
        client_class = EnvJujuClient
864
 
    return client_class
865
 
 
866
 
 
867
 
def client_from_config(config, juju_path, debug=False, soft_deadline=None):
868
 
    """Create a client from an environment's configuration.
869
 
 
870
 
    :param config: Name of the environment to use the config from.
871
 
    :param juju_path: Path to juju binary the client should wrap.
872
 
    :param debug=False: The debug flag for the client, False by default.
873
 
    :param soft_deadline: A datetime representing the deadline by which
874
 
        normal operations should complete.  If None, no deadline is
875
 
        enforced.
876
 
    """
877
 
    version = EnvJujuClient.get_version(juju_path)
878
 
    client_class = get_client_class(version)
879
 
    env = client_class.config_class.from_config(config)
880
 
    if juju_path is None:
881
 
        full_path = EnvJujuClient.get_full_path()
882
 
    else:
883
 
        full_path = os.path.abspath(juju_path)
884
 
    return client_class(env, version, full_path, debug=debug,
885
 
                        soft_deadline=soft_deadline)
886
 
 
887
 
 
888
154
class EnvJujuClient:
889
 
    """Wraps calls to a juju instance, associated with a single model.
890
 
 
891
 
    Note: A model is often called an enviroment (Juju 1 legacy).
892
 
 
893
 
    This class represents the latest Juju version.  Subclasses are used to
894
 
    support older versions (see get_client_class).
895
 
    """
896
 
 
897
 
    # The environments.yaml options that are replaced by bootstrap options.
898
 
    #
899
 
    # As described in bug #1538735, default-series and --bootstrap-series must
900
 
    # match.  'default-series' should be here, but is omitted so that
901
 
    # default-series is always forced to match --bootstrap-series.
902
 
    bootstrap_replaces = frozenset(['agent-version'])
903
 
 
904
 
    # What feature flags have existed that CI used.
905
 
    known_feature_flags = frozenset([
906
 
        'actions', 'jes', 'address-allocation', 'cloudsigma', 'migration'])
907
 
 
908
 
    # What feature flags are used by this version of the juju client.
909
 
    used_feature_flags = frozenset(['address-allocation', 'migration'])
910
 
 
911
 
    destroy_model_command = 'destroy-model'
912
 
 
913
 
    supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE,
914
 
                                           LXD_MACHINE])
915
 
 
916
 
    default_backend = Juju2Backend
917
 
 
918
 
    config_class = JujuData
919
 
 
920
 
    status_class = Status
921
 
 
922
 
    agent_metadata_url = 'agent-metadata-url'
923
 
 
924
 
    model_permissions = frozenset(['read', 'write', 'admin'])
925
 
 
926
 
    controller_permissions = frozenset(['login', 'addmodel', 'superuser'])
927
 
 
928
 
    @classmethod
929
 
    def preferred_container(cls):
930
 
        for container_type in [LXD_MACHINE, LXC_MACHINE]:
931
 
            if container_type in cls.supported_container_types:
932
 
                return container_type
933
 
 
934
 
    _show_status = 'show-status'
935
155
 
936
156
    @classmethod
937
157
    def get_version(cls, juju_path=None):
938
 
        """Get the version data from a juju binary.
939
 
 
940
 
        :param juju_path: Path to binary. If not given or None, 'juju' is used.
941
 
        """
942
158
        if juju_path is None:
943
159
            juju_path = 'juju'
944
160
        return subprocess.check_output((juju_path, '--version')).strip()
945
161
 
946
 
    def check_timeouts(self):
947
 
        return self._backend._check_timeouts()
948
 
 
949
 
    def ignore_soft_deadline(self):
950
 
        return self._backend.ignore_soft_deadline()
951
 
 
952
 
    def enable_feature(self, flag):
953
 
        """Enable juju feature by setting the given flag.
954
 
 
955
 
        New versions of juju with the feature enabled by default will silently
956
 
        allow this call, but will not export the environment variable.
957
 
        """
958
 
        if flag not in self.known_feature_flags:
959
 
            raise ValueError('Unknown feature flag: %r' % (flag,))
960
 
        self.feature_flags.add(flag)
961
 
 
962
 
    def get_jes_command(self):
963
 
        """For Juju 2.0, this is always kill-controller."""
964
 
        return KILL_CONTROLLER
965
 
 
966
162
    def is_jes_enabled(self):
967
163
        """Does the state-server support multiple environments."""
968
164
        try:
971
167
        except JESNotSupported:
972
168
            return False
973
169
 
 
170
    def get_jes_command(self):
 
171
        """Return the JES command to destroy a controller.
 
172
 
 
173
        Juju 2.x has 'controller kill'.
 
174
        Juju 1.26 has 'destroy-controller'.
 
175
        Juju 1.25 has 'system kill' when the jes feature flag is set.
 
176
 
 
177
        :raises: JESNotSupported when the version of Juju does not expose
 
178
            a JES command.
 
179
        :return: The JES command.
 
180
        """
 
181
        commands = self.get_juju_output('help', 'commands', include_e=False)
 
182
        for line in commands.splitlines():
 
183
            for cmd in _jes_cmds.keys():
 
184
                if line.startswith(cmd):
 
185
                    return cmd
 
186
        raise JESNotSupported()
 
187
 
974
188
    def enable_jes(self):
975
189
        """Enable JES if JES is optional.
976
190
 
993
207
            return WIN_JUJU_CMD
994
208
        return subprocess.check_output(('which', 'juju')).rstrip('\n')
995
209
 
996
 
    def clone_path_cls(self, juju_path):
997
 
        """Clone using the supplied path to determine the class."""
998
 
        version = self.get_version(juju_path)
999
 
        cls = get_client_class(version)
 
210
    @classmethod
 
211
    def by_version(cls, env, juju_path=None, debug=False):
 
212
        version = cls.get_version(juju_path)
1000
213
        if juju_path is None:
1001
 
            full_path = self.get_full_path()
 
214
            full_path = cls.get_full_path()
1002
215
        else:
1003
216
            full_path = os.path.abspath(juju_path)
1004
 
        return self.clone(version=version, full_path=full_path, cls=cls)
1005
 
 
1006
 
    def clone(self, env=None, version=None, full_path=None, debug=None,
1007
 
              cls=None):
1008
 
        """Create a clone of this EnvJujuClient.
1009
 
 
1010
 
        By default, the class, environment, version, full_path, and debug
1011
 
        settings will match the original, but each can be overridden.
1012
 
        """
1013
 
        if env is None:
1014
 
            env = self.env
1015
 
        if cls is None:
1016
 
            cls = self.__class__
1017
 
        feature_flags = self.feature_flags.intersection(cls.used_feature_flags)
1018
 
        backend = self._backend.clone(full_path, version, debug, feature_flags)
1019
 
        other = cls.from_backend(backend, env)
1020
 
        return other
1021
 
 
1022
 
    @classmethod
1023
 
    def from_backend(cls, backend, env):
1024
 
        return cls(env=env, version=backend.version,
1025
 
                   full_path=backend.full_path,
1026
 
                   debug=backend.debug, _backend=backend)
1027
 
 
1028
 
    def get_cache_path(self):
1029
 
        return get_cache_path(self.env.juju_home, models=True)
1030
 
 
1031
 
    def _cmd_model(self, include_e, controller):
1032
 
        if controller:
1033
 
            return '{controller}:{model}'.format(
1034
 
                controller=self.env.controller.name,
1035
 
                model=self.get_controller_model_name())
1036
 
        elif self.env is None or not include_e:
1037
 
            return None
 
217
        if version.startswith('1.16'):
 
218
            raise Exception('Unsupported juju: %s' % version)
 
219
        elif re.match('^1\.22[.-]', version):
 
220
            client_class = EnvJujuClient22
 
221
        elif re.match('^1\.24[.-]', version):
 
222
            client_class = EnvJujuClient24
 
223
        elif re.match('^1\.25[.-]', version):
 
224
            client_class = EnvJujuClient25
 
225
        elif re.match('^1\.26[.-]', version):
 
226
            client_class = EnvJujuClient26
1038
227
        else:
1039
 
            return '{controller}:{model}'.format(
1040
 
                controller=self.env.controller.name,
1041
 
                model=self.model_name)
 
228
            client_class = EnvJujuClient
 
229
        return client_class(env, version, full_path, get_juju_home(),
 
230
                            debug=debug)
1042
231
 
1043
 
    def _full_args(self, command, sudo, args,
1044
 
                   timeout=None, include_e=True, controller=False):
1045
 
        model = self._cmd_model(include_e, controller)
 
232
    def _full_args(self, command, sudo, args, timeout=None, include_e=True):
1046
233
        # sudo is not needed for devel releases.
1047
 
        return self._backend.full_args(command, args, model, timeout)
1048
 
 
1049
 
    @staticmethod
1050
 
    def _get_env(env):
1051
 
        if not isinstance(env, JujuData) and isinstance(env,
1052
 
                                                        SimpleEnvironment):
1053
 
            # FIXME: JujuData should be used from the start.
1054
 
            env = JujuData.from_env(env)
1055
 
        return env
1056
 
 
1057
 
    def __init__(self, env, version, full_path, juju_home=None, debug=False,
1058
 
                 soft_deadline=None, _backend=None):
1059
 
        """Create a new juju client.
1060
 
 
1061
 
        Required Arguments
1062
 
        :param env: Object representing a model in a data directory.
1063
 
        :param version: Version of juju the client wraps.
1064
 
        :param full_path: Full path to juju binary.
1065
 
 
1066
 
        Optional Arguments
1067
 
        :param juju_home: default value for env.juju_home.  Will be
1068
 
            autodetected if None (the default).
1069
 
        :param debug: Flag to activate debugging output; False by default.
1070
 
        :param soft_deadline: A datetime representing the deadline by which
1071
 
            normal operations should complete.  If None, no deadline is
1072
 
            enforced.
1073
 
        :param _backend: The backend to use for interacting with the client.
1074
 
            If None (the default), self.default_backend will be used.
1075
 
        """
1076
 
        self.env = self._get_env(env)
1077
 
        if _backend is None:
1078
 
            _backend = self.default_backend(full_path, version, set(), debug,
1079
 
                                            soft_deadline)
1080
 
        self._backend = _backend
1081
 
        if version != _backend.version:
1082
 
            raise ValueError('Version mismatch: {} {}'.format(
1083
 
                version, _backend.version))
1084
 
        if full_path != _backend.full_path:
1085
 
            raise ValueError('Path mismatch: {} {}'.format(
1086
 
                full_path, _backend.full_path))
1087
 
        if debug is not _backend.debug:
1088
 
            raise ValueError('debug mismatch: {} {}'.format(
1089
 
                debug, _backend.debug))
 
234
        if self.env is None or not include_e:
 
235
            e_arg = ()
 
236
        else:
 
237
            e_arg = ('-e', self.env.environment)
 
238
        if timeout is None:
 
239
            prefix = ()
 
240
        else:
 
241
            prefix = get_timeout_prefix(timeout, self._timeout_path)
 
242
        logging = '--debug' if self.debug else '--show-log'
 
243
 
 
244
        # If args is a string, make it a tuple. This makes writing commands
 
245
        # with one argument a bit nicer.
 
246
        if isinstance(args, basestring):
 
247
            args = (args,)
 
248
        # we split the command here so that the caller can control where the -e
 
249
        # <env> flag goes.  Everything in the command string is put before the
 
250
        # -e flag.
 
251
        command = command.split()
 
252
        return prefix + ('juju', logging,) + tuple(command) + e_arg + args
 
253
 
 
254
    def __init__(self, env, version, full_path, juju_home=None, debug=False):
 
255
        self.env = env
 
256
        self.version = version
 
257
        self.full_path = full_path
 
258
        self.debug = debug
1090
259
        if env is not None:
1091
260
            if juju_home is None:
1092
261
                if env.juju_home is None:
1093
262
                    env.juju_home = get_juju_home()
1094
263
            else:
1095
264
                env.juju_home = juju_home
1096
 
 
1097
 
    @property
1098
 
    def version(self):
1099
 
        return self._backend.version
1100
 
 
1101
 
    @property
1102
 
    def full_path(self):
1103
 
        return self._backend.full_path
1104
 
 
1105
 
    @property
1106
 
    def feature_flags(self):
1107
 
        return self._backend.feature_flags
1108
 
 
1109
 
    @feature_flags.setter
1110
 
    def feature_flags(self, feature_flags):
1111
 
        self._backend.feature_flags = feature_flags
1112
 
 
1113
 
    @property
1114
 
    def debug(self):
1115
 
        return self._backend.debug
1116
 
 
1117
 
    @property
1118
 
    def model_name(self):
1119
 
        return self.env.environment
 
265
        self.juju_timings = {}
 
266
        self._timeout_path = get_timeout_path()
1120
267
 
1121
268
    def _shell_environ(self):
1122
269
        """Generate a suitable shell environment.
1123
270
 
1124
271
        Juju's directory must be in the PATH to support plugins.
1125
272
        """
1126
 
        return self._backend.shell_environ(self.used_feature_flags,
1127
 
                                           self.env.juju_home)
 
273
        env = dict(os.environ)
 
274
        if self.full_path is not None:
 
275
            env['PATH'] = '{}{}{}'.format(os.path.dirname(self.full_path),
 
276
                                          os.pathsep, env['PATH'])
 
277
        env['JUJU_HOME'] = self.env.juju_home
 
278
        return env
1128
279
 
1129
280
    def add_ssh_machines(self, machines):
1130
 
        for count, machine in enumerate(machines):
1131
 
            try:
1132
 
                self.juju('add-machine', ('ssh:' + machine,))
1133
 
            except subprocess.CalledProcessError:
1134
 
                if count != 0:
1135
 
                    raise
1136
 
                logging.warning('add-machine failed.  Will retry.')
1137
 
                pause(30)
1138
 
                self.juju('add-machine', ('ssh:' + machine,))
1139
 
 
1140
 
    @staticmethod
1141
 
    def get_cloud_region(cloud, region):
1142
 
        if region is None:
1143
 
            return cloud
1144
 
        return '{}/{}'.format(cloud, region)
1145
 
 
1146
 
    def get_bootstrap_args(
1147
 
            self, upload_tools, config_filename, bootstrap_series=None,
1148
 
            credential=None, auto_upgrade=False, metadata_source=None,
1149
 
            to=None, agent_version=None):
1150
 
        """Return the bootstrap arguments for the substrate."""
1151
 
        if self.env.joyent:
 
281
        for machine in machines:
 
282
            self.juju('add-machine', ('ssh:' + machine,))
 
283
 
 
284
    def get_bootstrap_args(self, upload_tools):
 
285
        """Bootstrap, using sudo if necessary."""
 
286
        if self.env.hpcloud:
 
287
            constraints = 'mem=2G'
 
288
        elif self.env.maas:
 
289
            constraints = 'mem=2G arch=amd64'
 
290
        elif self.env.joyent:
1152
291
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1153
292
            constraints = 'mem=2G cpu-cores=1'
1154
293
        else:
1155
294
            constraints = 'mem=2G'
1156
 
        cloud_region = self.get_cloud_region(self.env.get_cloud(),
1157
 
                                             self.env.get_region())
1158
 
        args = ['--constraints', constraints, self.env.environment,
1159
 
                cloud_region, '--config', config_filename,
1160
 
                '--default-model', self.env.environment]
 
295
        args = ('--constraints', constraints)
1161
296
        if upload_tools:
1162
 
            if agent_version is not None:
1163
 
                raise ValueError(
1164
 
                    'agent-version may not be given with upload-tools.')
1165
 
            args.insert(0, '--upload-tools')
1166
 
        else:
1167
 
            if agent_version is None:
1168
 
                agent_version = self.get_matching_agent_version()
1169
 
            args.extend(['--agent-version', agent_version])
1170
 
        if bootstrap_series is not None:
1171
 
            args.extend(['--bootstrap-series', bootstrap_series])
1172
 
        if credential is not None:
1173
 
            args.extend(['--credential', credential])
1174
 
        if metadata_source is not None:
1175
 
            args.extend(['--metadata-source', metadata_source])
1176
 
        if auto_upgrade:
1177
 
            args.append('--auto-upgrade')
1178
 
        if to is not None:
1179
 
            args.extend(['--to', to])
1180
 
        return tuple(args)
1181
 
 
1182
 
    def add_model(self, env):
1183
 
        """Add a model to this model's controller and return its client.
1184
 
 
1185
 
        :param env: Class representing the new model/environment."""
1186
 
        model_client = self.clone(env)
1187
 
        with model_client._bootstrap_config() as config_file:
1188
 
            self._add_model(env.environment, config_file)
1189
 
        return model_client
1190
 
 
1191
 
    def make_model_config(self):
1192
 
        config_dict = make_safe_config(self)
1193
 
        agent_metadata_url = config_dict.pop('tools-metadata-url', None)
1194
 
        if agent_metadata_url is not None:
1195
 
            config_dict.setdefault('agent-metadata-url', agent_metadata_url)
1196
 
        # Strip unneeded variables.
1197
 
        return dict((k, v) for k, v in config_dict.items() if k not in {
1198
 
            'access-key',
1199
 
            'admin-secret',
1200
 
            'application-id',
1201
 
            'application-password',
1202
 
            'auth-url',
1203
 
            'bootstrap-host',
1204
 
            'client-email',
1205
 
            'client-id',
1206
 
            'control-bucket',
1207
 
            'location',
1208
 
            'maas-oauth',
1209
 
            'maas-server',
1210
 
            'management-certificate',
1211
 
            'management-subscription-id',
1212
 
            'manta-key-id',
1213
 
            'manta-user',
1214
 
            'name',
1215
 
            'password',
1216
 
            'private-key',
1217
 
            'region',
1218
 
            'sdc-key-id',
1219
 
            'sdc-url',
1220
 
            'sdc-user',
1221
 
            'secret-key',
1222
 
            'storage-account-name',
1223
 
            'subscription-id',
1224
 
            'tenant-id',
1225
 
            'tenant-name',
1226
 
            'type',
1227
 
            'username',
1228
 
        })
1229
 
 
1230
 
    @contextmanager
1231
 
    def _bootstrap_config(self):
1232
 
        with temp_yaml_file(self.make_model_config()) as config_filename:
1233
 
            yield config_filename
1234
 
 
1235
 
    def _check_bootstrap(self):
1236
 
        if self.env.environment != self.env.controller.name:
1237
 
            raise AssertionError(
1238
 
                'Controller and environment names should not vary (yet)')
1239
 
 
1240
 
    def update_user_name(self):
1241
 
        self.env.user_name = 'admin@local'
1242
 
 
1243
 
    def bootstrap(self, upload_tools=False, bootstrap_series=None,
1244
 
                  credential=None, auto_upgrade=False, metadata_source=None,
1245
 
                  to=None, agent_version=None):
1246
 
        """Bootstrap a controller."""
1247
 
        self._check_bootstrap()
1248
 
        with self._bootstrap_config() as config_filename:
1249
 
            args = self.get_bootstrap_args(
1250
 
                upload_tools, config_filename, bootstrap_series, credential,
1251
 
                auto_upgrade, metadata_source, to, agent_version)
1252
 
            self.update_user_name()
1253
 
            self.juju('bootstrap', args, include_e=False)
1254
 
 
1255
 
    @contextmanager
1256
 
    def bootstrap_async(self, upload_tools=False, bootstrap_series=None,
1257
 
                        auto_upgrade=False, metadata_source=None, to=None):
1258
 
        self._check_bootstrap()
1259
 
        with self._bootstrap_config() as config_filename:
1260
 
            args = self.get_bootstrap_args(
1261
 
                upload_tools, config_filename, bootstrap_series, None,
1262
 
                auto_upgrade, metadata_source, to)
1263
 
            self.update_user_name()
1264
 
            with self.juju_async('bootstrap', args, include_e=False):
1265
 
                yield
1266
 
                log.info('Waiting for bootstrap of {}.'.format(
1267
 
                    self.env.environment))
1268
 
 
1269
 
    def _add_model(self, model_name, config_file):
1270
 
        self.controller_juju('add-model', (
1271
 
            model_name, '--config', config_file))
1272
 
 
1273
 
    def destroy_model(self):
 
297
            args = ('--upload-tools',) + args
 
298
        return args
 
299
 
 
300
    def bootstrap(self, upload_tools=False):
 
301
        args = self.get_bootstrap_args(upload_tools)
 
302
        self.juju('bootstrap', args, self.env.needs_sudo())
 
303
 
 
304
    @contextmanager
 
305
    def bootstrap_async(self, upload_tools=False):
 
306
        args = self.get_bootstrap_args(upload_tools)
 
307
        with self.juju_async('bootstrap', args):
 
308
            yield
 
309
            log.info('Waiting for bootstrap of {}.'.format(
 
310
                self.env.environment))
 
311
 
 
312
    def create_environment(self, controller_client, config_file):
 
313
        seen_cmd = self.get_jes_command()
 
314
        if seen_cmd == OPTIONAL_JES_COMMAND:
 
315
            controller_option = ('-s', controller_client.env.environment)
 
316
        else:
 
317
            controller_option = ('-c', controller_client.env.environment)
 
318
        self.juju(_jes_cmds[seen_cmd]['create'], controller_option + (
 
319
            self.env.environment, '--config', config_file), include_e=False)
 
320
 
 
321
    def destroy_environment(self, force=True, delete_jenv=False):
 
322
        if force:
 
323
            force_arg = ('--force',)
 
324
        else:
 
325
            force_arg = ()
1274
326
        exit_status = self.juju(
1275
 
            'destroy-model', (self.env.environment, '-y',),
1276
 
            include_e=False, timeout=get_teardown_timeout(self))
 
327
            'destroy-environment',
 
328
            (self.env.environment,) + force_arg + ('-y',),
 
329
            self.env.needs_sudo(), check=False, include_e=False,
 
330
            timeout=timedelta(minutes=10).total_seconds())
 
331
        if delete_jenv:
 
332
            jenv_path = get_jenv_path(self.env.juju_home, self.env.environment)
 
333
            ensure_deleted(jenv_path)
1277
334
        return exit_status
1278
335
 
1279
336
    def kill_controller(self):
1280
337
        """Kill a controller and its environments."""
1281
338
        seen_cmd = self.get_jes_command()
1282
339
        self.juju(
1283
 
            _jes_cmds[seen_cmd]['kill'], (self.env.controller.name, '-y'),
1284
 
            include_e=False, check=False, timeout=get_teardown_timeout(self))
 
340
            _jes_cmds[seen_cmd]['kill'], (self.env.environment, '-y'),
 
341
            include_e=False, check=False, timeout=600)
1285
342
 
1286
343
    def get_juju_output(self, command, *args, **kwargs):
1287
344
        """Call a juju command and return the output.
1290
347
        that <command> may be a space delimited list of arguments. The -e
1291
348
        <environment> flag will be placed after <command> and before args.
1292
349
        """
1293
 
        model = self._cmd_model(kwargs.get('include_e', True),
1294
 
                                kwargs.get('controller', False))
1295
 
        pass_kwargs = dict(
1296
 
            (k, kwargs[k]) for k in kwargs if k in ['timeout', 'merge_stderr'])
1297
 
        return self._backend.get_juju_output(
1298
 
            command, args, self.used_feature_flags, self.env.juju_home,
1299
 
            model, user_name=self.env.user_name, **pass_kwargs)
1300
 
 
1301
 
    def show_status(self):
1302
 
        """Print the status to output."""
1303
 
        self.juju(self._show_status, ('--format', 'yaml'))
1304
 
 
1305
 
    def get_status(self, timeout=60, raw=False, controller=False, *args):
 
350
        args = self._full_args(command, False, args,
 
351
                               timeout=kwargs.get('timeout'),
 
352
                               include_e=kwargs.get('include_e', True))
 
353
        env = self._shell_environ()
 
354
        log.debug(args)
 
355
        # Mutate os.environ instead of supplying env parameter so
 
356
        # Windows can search env['PATH']
 
357
        with scoped_environ(env):
 
358
            proc = subprocess.Popen(
 
359
                args, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
 
360
                stderr=subprocess.PIPE)
 
361
            sub_output, sub_error = proc.communicate()
 
362
            log.debug(sub_output)
 
363
            if proc.returncode != 0:
 
364
                log.debug(sub_error)
 
365
                e = subprocess.CalledProcessError(
 
366
                    proc.returncode, args[0], sub_error)
 
367
                e.stderr = sub_error
 
368
                if (
 
369
                    'Unable to connect to environment' in sub_error or
 
370
                        'MissingOrIncorrectVersionHeader' in sub_error or
 
371
                        '307: Temporary Redirect' in sub_error):
 
372
                    raise CannotConnectEnv(e)
 
373
                raise e
 
374
        return sub_output
 
375
 
 
376
    def get_status(self, timeout=60, raw=False, *args):
1306
377
        """Get the current status as a dict."""
1307
378
        # GZ 2015-12-16: Pass remaining timeout into get_juju_output call.
1308
379
        for ignored in until_timeout(timeout):
1309
380
            try:
1310
381
                if raw:
1311
 
                    return self.get_juju_output(self._show_status, *args)
1312
 
                return self.status_class.from_text(
1313
 
                    self.get_juju_output(
1314
 
                        self._show_status, '--format', 'yaml',
1315
 
                        controller=controller))
 
382
                    return self.get_juju_output('status', *args)
 
383
                return Status.from_text(self.get_juju_output('status'))
1316
384
            except subprocess.CalledProcessError:
1317
385
                pass
1318
386
        raise Exception(
1319
387
            'Timed out waiting for juju status to succeed')
1320
388
 
1321
 
    @staticmethod
1322
 
    def _dict_as_option_strings(options):
1323
 
        return tuple('{}={}'.format(*item) for item in options.items())
1324
 
 
1325
 
    def set_config(self, service, options):
1326
 
        option_strings = self._dict_as_option_strings(options)
1327
 
        self.juju('config', (service,) + option_strings)
1328
 
 
1329
 
    def get_config(self, service):
1330
 
        return yaml_loads(self.get_juju_output('config', service))
1331
 
 
1332
389
    def get_service_config(self, service, timeout=60):
1333
390
        for ignored in until_timeout(timeout):
1334
391
            try:
1335
 
                return self.get_config(service)
 
392
                return yaml_loads(self.get_juju_output('get', service))
1336
393
            except subprocess.CalledProcessError:
1337
394
                pass
1338
395
        raise Exception(
1339
396
            'Timed out waiting for juju get %s' % (service))
1340
397
 
1341
 
    def set_model_constraints(self, constraints):
1342
 
        constraint_strings = self._dict_as_option_strings(constraints)
1343
 
        return self.juju('set-model-constraints', constraint_strings)
1344
 
 
1345
 
    def get_model_config(self):
1346
 
        """Return the value of the environment's configured options."""
1347
 
        return yaml.safe_load(
1348
 
            self.get_juju_output('model-config', '--format', 'yaml'))
1349
 
 
1350
398
    def get_env_option(self, option):
1351
399
        """Return the value of the environment's configured option."""
1352
 
        return self.get_juju_output('model-config', option)
 
400
        return self.get_juju_output('get-env', option)
1353
401
 
1354
402
    def set_env_option(self, option, value):
1355
403
        """Set the value of the option in the environment."""
1356
404
        option_value = "%s=%s" % (option, value)
1357
 
        return self.juju('model-config', (option_value,))
1358
 
 
1359
 
    def unset_env_option(self, option):
1360
 
        """Unset the value of the option in the environment."""
1361
 
        return self.juju('model-config', ('--reset', option,))
1362
 
 
1363
 
    def get_agent_metadata_url(self):
1364
 
        return self.get_env_option(self.agent_metadata_url)
1365
 
 
1366
 
    def set_testing_agent_metadata_url(self):
1367
 
        url = self.get_agent_metadata_url()
 
405
        return self.juju('set-env', (option_value,))
 
406
 
 
407
    def set_testing_tools_metadata_url(self):
 
408
        url = self.get_env_option('tools-metadata-url')
1368
409
        if 'testing' not in url:
1369
410
            testing_url = url.replace('/tools', '/testing/tools')
1370
 
            self.set_env_option(self.agent_metadata_url, testing_url)
 
411
            self.set_env_option('tools-metadata-url', testing_url)
1371
412
 
1372
413
    def juju(self, command, args, sudo=False, check=True, include_e=True,
1373
414
             timeout=None, extra_env=None):
1374
415
        """Run a command under juju for the current environment."""
1375
 
        model = self._cmd_model(include_e, controller=False)
1376
 
        return self._backend.juju(
1377
 
            command, args, self.used_feature_flags, self.env.juju_home,
1378
 
            model, check, timeout, extra_env)
1379
 
 
1380
 
    def expect(self, command, args=(), sudo=False, include_e=True,
1381
 
               timeout=None, extra_env=None):
1382
 
        """Return a process object that is running an interactive `command`.
1383
 
 
1384
 
        The interactive command ability is provided by using pexpect.
1385
 
 
1386
 
        :param command: String of the juju command to run.
1387
 
        :param args: Tuple containing arguments for the juju `command`.
1388
 
        :param sudo: Whether to call `command` using sudo.
1389
 
        :param include_e: Boolean regarding supplying the juju environment to
1390
 
          `command`.
1391
 
        :param timeout: A float that, if provided, is the timeout in which the
1392
 
          `command` is run.
1393
 
 
1394
 
        :return: A pexpect.spawn object that has been called with `command` and
1395
 
          `args`.
1396
 
 
1397
 
        """
1398
 
        model = self._cmd_model(include_e, controller=False)
1399
 
        return self._backend.expect(
1400
 
            command, args, self.used_feature_flags, self.env.juju_home,
1401
 
            model, timeout, extra_env)
1402
 
 
1403
 
    def controller_juju(self, command, args):
1404
 
        args = ('-c', self.env.controller.name) + args
1405
 
        return self.juju(command, args, include_e=False)
 
416
        args = self._full_args(command, sudo, args, include_e=include_e,
 
417
                               timeout=timeout)
 
418
        log.info(' '.join(args))
 
419
        env = self._shell_environ()
 
420
        if extra_env is not None:
 
421
            env.update(extra_env)
 
422
        if check:
 
423
            call_func = subprocess.check_call
 
424
        else:
 
425
            call_func = subprocess.call
 
426
        start_time = time.time()
 
427
        # Mutate os.environ instead of supplying env parameter so Windows can
 
428
        # search env['PATH']
 
429
        with scoped_environ(env):
 
430
            rval = call_func(args)
 
431
        self.juju_timings.setdefault(args, []).append(
 
432
            (time.time() - start_time))
 
433
        return rval
1406
434
 
1407
435
    def get_juju_timings(self):
1408
436
        stringified_timings = {}
1409
 
        for command, timings in self._backend.juju_timings.items():
 
437
        for command, timings in self.juju_timings.items():
1410
438
            stringified_timings[' '.join(command)] = timings
1411
439
        return stringified_timings
1412
440
 
 
441
    @contextmanager
1413
442
    def juju_async(self, command, args, include_e=True, timeout=None):
1414
 
        model = self._cmd_model(include_e, controller=False)
1415
 
        return self._backend.juju_async(command, args, self.used_feature_flags,
1416
 
                                        self.env.juju_home, model, timeout)
 
443
        full_args = self._full_args(command, False, args, include_e=include_e,
 
444
                                    timeout=timeout)
 
445
        log.info(' '.join(args))
 
446
        env = self._shell_environ()
 
447
        # Mutate os.environ instead of supplying env parameter so Windows can
 
448
        # search env['PATH']
 
449
        with scoped_environ(env):
 
450
            proc = subprocess.Popen(full_args)
 
451
        yield proc
 
452
        retcode = proc.wait()
 
453
        if retcode != 0:
 
454
            raise subprocess.CalledProcessError(retcode, full_args)
1417
455
 
1418
 
    def deploy(self, charm, repository=None, to=None, series=None,
1419
 
               service=None, force=False, resource=None,
1420
 
               storage=None, constraints=None):
 
456
    def deploy(self, charm, repository=None, to=None, service=None):
1421
457
        args = [charm]
 
458
        if repository is not None:
 
459
            args.extend(['--repository', repository])
 
460
        if to is not None:
 
461
            args.extend(['--to', to])
1422
462
        if service is not None:
1423
463
            args.extend([service])
1424
 
        if to is not None:
1425
 
            args.extend(['--to', to])
1426
 
        if series is not None:
1427
 
            args.extend(['--series', series])
1428
 
        if force is True:
1429
 
            args.extend(['--force'])
1430
 
        if resource is not None:
1431
 
            args.extend(['--resource', resource])
1432
 
        if storage is not None:
1433
 
            args.extend(['--storage', storage])
1434
 
        if constraints is not None:
1435
 
            args.extend(['--constraints', constraints])
1436
464
        return self.juju('deploy', tuple(args))
1437
465
 
1438
 
    def attach(self, service, resource):
1439
 
        args = (service, resource)
1440
 
        return self.juju('attach', args)
1441
 
 
1442
 
    def list_resources(self, service_or_unit, details=True):
1443
 
        args = ('--format', 'yaml', service_or_unit)
1444
 
        if details:
1445
 
            args = args + ('--details',)
1446
 
        return yaml_loads(self.get_juju_output('list-resources', *args))
1447
 
 
1448
 
    def wait_for_resource(self, resource_id, service_or_unit, timeout=60):
1449
 
        log.info('Waiting for resource. Resource id:{}'.format(resource_id))
1450
 
        with self.check_timeouts():
1451
 
            with self.ignore_soft_deadline():
1452
 
                for _ in until_timeout(timeout):
1453
 
                    resources_dict = self.list_resources(service_or_unit)
1454
 
                    resources = resources_dict['resources']
1455
 
                    for resource in resources:
1456
 
                        if resource['expected']['resourceid'] == resource_id:
1457
 
                            if (resource['expected']['fingerprint'] ==
1458
 
                                    resource['unit']['fingerprint']):
1459
 
                                return
1460
 
                    time.sleep(.1)
1461
 
                raise JujuResourceTimeout(
1462
 
                    'Timeout waiting for a resource to be downloaded. '
1463
 
                    'ResourceId: {} Service or Unit: {} Timeout: {}'.format(
1464
 
                        resource_id, service_or_unit, timeout))
1465
 
 
1466
 
    def upgrade_charm(self, service, charm_path=None):
1467
 
        args = (service,)
1468
 
        if charm_path is not None:
1469
 
            args = args + ('--path', charm_path)
1470
 
        self.juju('upgrade-charm', args)
1471
 
 
1472
 
    def remove_service(self, service):
1473
 
        self.juju('remove-application', (service,))
1474
 
 
1475
 
    @classmethod
1476
 
    def format_bundle(cls, bundle_template):
1477
 
        return bundle_template.format(container=cls.preferred_container())
1478
 
 
1479
 
    def deploy_bundle(self, bundle_template, timeout=_DEFAULT_BUNDLE_TIMEOUT):
1480
 
        """Deploy bundle using native juju 2.0 deploy command."""
1481
 
        bundle = self.format_bundle(bundle_template)
1482
 
        self.juju('deploy', bundle, timeout=timeout)
1483
 
 
1484
 
    def deployer(self, bundle_template, name=None, deploy_delay=10,
1485
 
                 timeout=3600):
1486
 
        """Deploy a bundle using deployer."""
1487
 
        bundle = self.format_bundle(bundle_template)
 
466
    def deployer(self, bundle, name=None, deploy_delay=10, timeout=3600):
 
467
        """deployer, using sudo if necessary."""
1488
468
        args = (
1489
469
            '--debug',
1490
470
            '--deploy-delay', str(deploy_delay),
1493
473
        )
1494
474
        if name:
1495
475
            args += (name,)
1496
 
        e_arg = ('-e', '{}:{}'.format(
1497
 
            self.env.controller.name, self.env.environment))
1498
 
        args = e_arg + args
1499
 
        self.juju('deployer', args, self.env.needs_sudo(), include_e=False)
1500
 
 
1501
 
    def _get_substrate_constraints(self):
1502
 
        if self.env.joyent:
1503
 
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1504
 
            return 'mem=2G cpu-cores=1'
1505
 
        else:
1506
 
            return 'mem=2G'
1507
 
 
1508
 
    def quickstart(self, bundle_template, upload_tools=False):
 
476
        self.juju('deployer', args, self.env.needs_sudo())
 
477
 
 
478
    def quickstart(self, bundle, upload_tools=False):
1509
479
        """quickstart, using sudo if necessary."""
1510
 
        bundle = self.format_bundle(bundle_template)
1511
 
        constraints = 'mem=2G'
 
480
        if self.env.maas:
 
481
            constraints = 'mem=2G arch=amd64'
 
482
        else:
 
483
            constraints = 'mem=2G'
1512
484
        args = ('--constraints', constraints)
1513
485
        if upload_tools:
1514
486
            args = ('--upload-tools',) + args
1527
499
        :param start: If supplied, the time to count from when determining
1528
500
            timeout.
1529
501
        """
1530
 
        with self.check_timeouts():
1531
 
            with self.ignore_soft_deadline():
1532
 
                yield self.get_status()
1533
 
                for remaining in until_timeout(timeout, start=start):
1534
 
                    yield self.get_status()
 
502
        yield self.get_status()
 
503
        for remaining in until_timeout(timeout, start=start):
 
504
            yield self.get_status()
1535
505
 
1536
506
    def _wait_for_status(self, reporter, translate, exc_type=StatusNotMet,
1537
507
                         timeout=1200, start=None):
1549
519
        """
1550
520
        status = None
1551
521
        try:
1552
 
            with self.check_timeouts():
1553
 
                with self.ignore_soft_deadline():
1554
 
                    for _ in chain([None],
1555
 
                                   until_timeout(timeout, start=start)):
1556
 
                        try:
1557
 
                            status = self.get_status()
1558
 
                        except CannotConnectEnv:
1559
 
                            log.info(
1560
 
                                'Suppressing "Unable to connect to'
1561
 
                                ' environment"')
1562
 
                            continue
1563
 
                        states = translate(status)
1564
 
                        if states is None:
1565
 
                            break
1566
 
                        reporter.update(states)
1567
 
                    else:
1568
 
                        if status is not None:
1569
 
                            log.error(status.status_text)
1570
 
                        raise exc_type(self.env.environment, status)
 
522
            for _ in chain([None], until_timeout(timeout, start=start)):
 
523
                try:
 
524
                    status = self.get_status()
 
525
                except CannotConnectEnv:
 
526
                    log.info('Suppressing "Unable to connect to environment"')
 
527
                    continue
 
528
                states = translate(status)
 
529
                if states is None:
 
530
                    break
 
531
                reporter.update(states)
 
532
            else:
 
533
                if status is not None:
 
534
                    log.error(status.status_text)
 
535
                raise exc_type(self.env.environment, status)
1571
536
        finally:
1572
537
            reporter.finish()
1573
538
        return status
1590
555
            for name, unit in status.service_subordinate_units(service):
1591
556
                if name.startswith(unit_prefix + '/'):
1592
557
                    subordinate_unit_count += 1
1593
 
                    unit_states[coalesce_agent_status(unit)].append(name)
 
558
                    unit_states[unit.get(
 
559
                        'agent-state', 'no-agent')].append(name)
1594
560
            if (subordinate_unit_count == service_unit_count and
1595
 
                    set(unit_states.keys()).issubset(AGENTS_READY)):
 
561
                    unit_states.keys() == ['started']):
1596
562
                return None
1597
563
            return unit_states
1598
564
        reporter = GroupReporter(sys.stdout, 'started')
1610
576
        self._wait_for_status(reporter, status_to_version, VersionsNotUpdated,
1611
577
                              timeout=timeout, start=start)
1612
578
 
1613
 
    def list_models(self):
1614
 
        """List the models registered with the current controller."""
1615
 
        self.controller_juju('list-models', ())
1616
 
 
1617
 
    def get_models(self):
1618
 
        """return a models dict with a 'models': [] key-value pair.
1619
 
 
1620
 
        The server has 120 seconds to respond because this method is called
1621
 
        often when tearing down a controller-less deployment.
1622
 
        """
1623
 
        output = self.get_juju_output(
1624
 
            'list-models', '-c', self.env.controller.name, '--format', 'yaml',
1625
 
            include_e=False, timeout=120)
1626
 
        models = yaml_loads(output)
1627
 
        return models
1628
 
 
1629
 
    def _get_models(self):
1630
 
        """return a list of model dicts."""
1631
 
        return self.get_models()['models']
1632
 
 
1633
 
    def iter_model_clients(self):
1634
 
        """Iterate through all the models that share this model's controller.
1635
 
 
1636
 
        Works only if JES is enabled.
1637
 
        """
1638
 
        models = self._get_models()
1639
 
        if not models:
1640
 
            yield self
1641
 
        for model in models:
1642
 
            yield self._acquire_model_client(model['name'])
1643
 
 
1644
 
    def get_controller_model_name(self):
1645
 
        """Return the name of the 'controller' model.
1646
 
 
1647
 
        Return the name of the environment when an 'controller' model does
1648
 
        not exist.
1649
 
        """
1650
 
        return 'controller'
1651
 
 
1652
 
    def _acquire_model_client(self, name):
1653
 
        """Get a client for a model with the supplied name.
1654
 
 
1655
 
        If the name matches self, self is used.  Otherwise, a clone is used.
1656
 
        """
1657
 
        if name == self.env.environment:
1658
 
            return self
1659
 
        else:
1660
 
            env = self.env.clone(model_name=name)
1661
 
            return self.clone(env=env)
1662
 
 
1663
 
    def get_model_uuid(self):
1664
 
        name = self.env.environment
1665
 
        model = self._cmd_model(True, False)
1666
 
        output_yaml = self.get_juju_output(
1667
 
            'show-model', '--format', 'yaml', model, include_e=False)
1668
 
        output = yaml.safe_load(output_yaml)
1669
 
        return output[name]['model-uuid']
1670
 
 
1671
 
    def get_controller_uuid(self):
1672
 
        name = self.env.controller.name
1673
 
        output_yaml = self.get_juju_output(
1674
 
            'show-controller', '--format', 'yaml', include_e=False)
1675
 
        output = yaml.safe_load(output_yaml)
1676
 
        return output[name]['details']['uuid']
1677
 
 
1678
 
    def get_controller_model_uuid(self):
1679
 
        output_yaml = self.get_juju_output(
1680
 
            'show-model', 'controller', '--format', 'yaml', include_e=False)
1681
 
        output = yaml.safe_load(output_yaml)
1682
 
        return output['controller']['model-uuid']
1683
 
 
1684
 
    def get_controller_client(self):
1685
 
        """Return a client for the controller model.  May return self.
1686
 
 
1687
 
        This may be inaccurate for models created using add_model
1688
 
        rather than bootstrap.
1689
 
        """
1690
 
        return self._acquire_model_client(self.get_controller_model_name())
1691
 
 
1692
 
    def list_controllers(self):
1693
 
        """List the controllers."""
1694
 
        self.juju('list-controllers', (), include_e=False)
1695
 
 
1696
 
    def get_controller_endpoint(self):
1697
 
        """Return the address of the controller leader."""
1698
 
        controller = self.env.controller.name
1699
 
        output = self.get_juju_output(
1700
 
            'show-controller', controller, include_e=False)
1701
 
        info = yaml_loads(output)
1702
 
        endpoint = info[controller]['details']['api-endpoints'][0]
1703
 
        address, port = split_address_port(endpoint)
1704
 
        return address
1705
 
 
1706
 
    def get_controller_members(self):
1707
 
        """Return a list of Machines that are members of the controller.
1708
 
 
1709
 
        The first machine in the list is the leader. the remaining machines
1710
 
        are followers in a HA relationship.
1711
 
        """
1712
 
        members = []
1713
 
        status = self.get_status()
1714
 
        for machine_id, machine in status.iter_machines():
1715
 
            if self.get_controller_member_status(machine):
1716
 
                members.append(Machine(machine_id, machine))
1717
 
        if len(members) <= 1:
1718
 
            return members
1719
 
        # Search for the leader and make it the first in the list.
1720
 
        # If the endpoint address is not the same as the leader's dns_name,
1721
 
        # the members are return in the order they were discovered.
1722
 
        endpoint = self.get_controller_endpoint()
1723
 
        log.debug('Controller endpoint is at {}'.format(endpoint))
1724
 
        members.sort(key=lambda m: m.info.get('dns-name') != endpoint)
1725
 
        return members
1726
 
 
1727
 
    def get_controller_leader(self):
1728
 
        """Return the controller leader Machine."""
1729
 
        controller_members = self.get_controller_members()
1730
 
        return controller_members[0]
1731
 
 
1732
 
    @staticmethod
1733
 
    def get_controller_member_status(info_dict):
1734
 
        """Return the controller-member-status of the machine if it exists."""
1735
 
        return info_dict.get('controller-member-status')
1736
 
 
1737
579
    def wait_for_ha(self, timeout=1200):
1738
580
        desired_state = 'has-vote'
1739
581
        reporter = GroupReporter(sys.stdout, desired_state)
1740
582
        try:
1741
 
            with self.check_timeouts():
1742
 
                with self.ignore_soft_deadline():
1743
 
                    for remaining in until_timeout(timeout):
1744
 
                        status = self.get_status(controller=True)
1745
 
                        status.check_agents_started()
1746
 
                        states = {}
1747
 
                        for machine, info in status.iter_machines():
1748
 
                            status = self.get_controller_member_status(info)
1749
 
                            if status is None:
1750
 
                                continue
1751
 
                            states.setdefault(status, []).append(machine)
1752
 
                        if states.keys() == [desired_state]:
1753
 
                            if len(states.get(desired_state, [])) >= 3:
1754
 
                                break
1755
 
                        reporter.update(states)
1756
 
                    else:
1757
 
                        raise Exception('Timed out waiting for voting to be'
1758
 
                                        ' enabled.')
 
583
            for remaining in until_timeout(timeout):
 
584
                status = self.get_status()
 
585
                states = {}
 
586
                for machine, info in status.iter_machines():
 
587
                    status = info.get('state-server-member-status')
 
588
                    if status is None:
 
589
                        continue
 
590
                    states.setdefault(status, []).append(machine)
 
591
                if states.keys() == [desired_state]:
 
592
                    if len(states.get(desired_state, [])) >= 3:
 
593
                        # XXX sinzui 2014-12-04: bug 1399277 happens because
 
594
                        # juju claims HA is ready when the monogo replica sets
 
595
                        # are not. Juju is not fully usable. The replica set
 
596
                        # lag might be 5 minutes.
 
597
                        pause(300)
 
598
                        return
 
599
                reporter.update(states)
 
600
            else:
 
601
                raise Exception('Timed out waiting for voting to be enabled.')
1759
602
        finally:
1760
603
            reporter.finish()
1761
 
        # XXX sinzui 2014-12-04: bug 1399277 happens because
1762
 
        # juju claims HA is ready when the monogo replica sets
1763
 
        # are not. Juju is not fully usable. The replica set
1764
 
        # lag might be 5 minutes.
1765
 
        self._backend.pause(300)
1766
604
 
1767
605
    def wait_for_deploy_started(self, service_count=1, timeout=1200):
1768
606
        """Wait until service_count services are 'started'.
1770
608
        :param service_count: The number of services for which to wait.
1771
609
        :param timeout: The number of seconds to wait.
1772
610
        """
1773
 
        with self.check_timeouts():
1774
 
            with self.ignore_soft_deadline():
1775
 
                for remaining in until_timeout(timeout):
1776
 
                    status = self.get_status()
1777
 
                    if status.get_service_count() >= service_count:
1778
 
                        return
1779
 
                else:
1780
 
                    raise Exception('Timed out waiting for services to start.')
 
611
        for remaining in until_timeout(timeout):
 
612
            status = self.get_status()
 
613
            if status.get_service_count() >= service_count:
 
614
                return
 
615
        else:
 
616
            raise Exception('Timed out waiting for services to start.')
1781
617
 
1782
 
    def wait_for_workloads(self, timeout=600, start=None):
 
618
    def wait_for_workloads(self, timeout=180, start=None):
1783
619
        """Wait until all unit workloads are in a ready state."""
1784
620
        def status_to_workloads(status):
1785
621
            unit_states = defaultdict(list)
1862
698
        args = ()
1863
699
        if force_version:
1864
700
            version = self.get_matching_agent_version(no_build=True)
1865
 
            args += ('--agent-version', version)
1866
 
        self._upgrade_juju(args)
1867
 
 
1868
 
    def _upgrade_juju(self, args):
 
701
            args += ('--version', version)
 
702
        if self.env.local:
 
703
            args += ('--upload-tools',)
1869
704
        self.juju('upgrade-juju', args)
1870
705
 
1871
 
    def upgrade_mongo(self):
1872
 
        self.juju('upgrade-mongo', ())
1873
 
 
1874
 
    def backup(self):
1875
 
        try:
1876
 
            output = self.get_juju_output('create-backup')
1877
 
        except subprocess.CalledProcessError as e:
1878
 
            log.info(e.output)
1879
 
            raise
1880
 
        log.info(output)
1881
 
        backup_file_pattern = re.compile('(juju-backup-[0-9-]+\.(t|tar.)gz)')
1882
 
        match = backup_file_pattern.search(output)
1883
 
        if match is None:
1884
 
            raise Exception("The backup file was not found in output: %s" %
1885
 
                            output)
1886
 
        backup_file_name = match.group(1)
1887
 
        backup_file_path = os.path.abspath(backup_file_name)
1888
 
        log.info("State-Server backup at %s", backup_file_path)
1889
 
        return backup_file_path
1890
 
 
1891
 
    def restore_backup(self, backup_file):
1892
 
        self.juju(
1893
 
            'restore-backup',
1894
 
            ('-b', '--constraints', 'mem=2G', '--file', backup_file))
1895
 
 
1896
 
    def restore_backup_async(self, backup_file):
1897
 
        return self.juju_async('restore-backup', ('-b', '--constraints',
1898
 
                               'mem=2G', '--file', backup_file))
1899
 
 
1900
 
    def enable_ha(self):
1901
 
        self.juju('enable-ha', ('-n', '3'))
1902
 
 
1903
 
    def action_fetch(self, id, action=None, timeout="1m"):
1904
 
        """Fetches the results of the action with the given id.
1905
 
 
1906
 
        Will wait for up to 1 minute for the action results.
1907
 
        The action name here is just used for an more informational error in
1908
 
        cases where it's available.
1909
 
        Returns the yaml output of the fetched action.
1910
 
        """
1911
 
        out = self.get_juju_output("show-action-output", id, "--wait", timeout)
1912
 
        status = yaml_loads(out)["status"]
1913
 
        if status != "completed":
1914
 
            name = ""
1915
 
            if action is not None:
1916
 
                name = " " + action
1917
 
            raise Exception(
1918
 
                "timed out waiting for action%s to complete during fetch" %
1919
 
                name)
1920
 
        return out
1921
 
 
1922
 
    def action_do(self, unit, action, *args):
1923
 
        """Performs the given action on the given unit.
1924
 
 
1925
 
        Action params should be given as args in the form foo=bar.
1926
 
        Returns the id of the queued action.
1927
 
        """
1928
 
        args = (unit, action) + args
1929
 
 
1930
 
        output = self.get_juju_output("run-action", *args)
1931
 
        action_id_pattern = re.compile(
1932
 
            'Action queued with id: ([a-f0-9\-]{36})')
1933
 
        match = action_id_pattern.search(output)
1934
 
        if match is None:
1935
 
            raise Exception("Action id not found in output: %s" %
1936
 
                            output)
1937
 
        return match.group(1)
1938
 
 
1939
 
    def action_do_fetch(self, unit, action, timeout="1m", *args):
1940
 
        """Performs given action on given unit and waits for the results.
1941
 
 
1942
 
        Action params should be given as args in the form foo=bar.
1943
 
        Returns the yaml output of the action.
1944
 
        """
1945
 
        id = self.action_do(unit, action, *args)
1946
 
        return self.action_fetch(id, action, timeout)
1947
 
 
1948
 
    def run(self, commands, applications):
1949
 
        responses = self.get_juju_output(
1950
 
            'run', '--format', 'json', '--application', ','.join(applications),
1951
 
            *commands)
1952
 
        return json.loads(responses)
1953
 
 
1954
 
    def list_space(self):
1955
 
        return yaml.safe_load(self.get_juju_output('list-space'))
1956
 
 
1957
 
    def add_space(self, space):
1958
 
        self.juju('add-space', (space),)
1959
 
 
1960
 
    def add_subnet(self, subnet, space):
1961
 
        self.juju('add-subnet', (subnet, space))
1962
 
 
1963
 
    def is_juju1x(self):
1964
 
        return self.version.startswith('1.')
1965
 
 
1966
 
    def _get_register_command(self, output):
1967
 
        """Return register token from add-user output.
1968
 
 
1969
 
        Return the register token supplied within the output from the add-user
1970
 
        command.
1971
 
 
1972
 
        """
1973
 
        for row in output.split('\n'):
1974
 
            if 'juju register' in row:
1975
 
                command_string = row.strip().lstrip()
1976
 
                command_parts = command_string.split(' ')
1977
 
                return command_parts[-1]
1978
 
        raise AssertionError('Juju register command not found in output')
1979
 
 
1980
 
    def add_user(self, username):
1981
 
        """Adds provided user and return register command arguments.
1982
 
 
1983
 
        :return: Registration token provided by the add-user command.
1984
 
        """
1985
 
        output = self.get_juju_output(
1986
 
            'add-user', username, '-c', self.env.controller.name,
1987
 
            include_e=False)
1988
 
        return self._get_register_command(output)
1989
 
 
1990
 
    def add_user_perms(self, username, models=None, permissions='login'):
1991
 
        """Adds provided user and return register command arguments.
1992
 
 
1993
 
        :return: Registration token provided by the add-user command.
1994
 
        """
1995
 
        output = self.add_user(username)
1996
 
        self.grant(username, permissions, models)
1997
 
        return output
1998
 
 
1999
 
    def revoke(self, username, models=None, permissions='read'):
2000
 
        if models is None:
2001
 
            models = self.env.environment
2002
 
 
2003
 
        args = (username, permissions, models)
2004
 
 
2005
 
        self.controller_juju('revoke', args)
2006
 
 
2007
 
    def add_storage(self, unit, storage_type, amount="1"):
2008
 
        """Add storage instances to service.
2009
 
 
2010
 
        Only type 'disk' is able to add instances.
2011
 
        """
2012
 
        self.juju('add-storage', (unit, storage_type + "=" + amount))
2013
 
 
2014
 
    def list_storage(self):
2015
 
        """Return the storage list."""
2016
 
        return self.get_juju_output('list-storage', '--format', 'json')
2017
 
 
2018
 
    def list_storage_pool(self):
2019
 
        """Return the list of storage pool."""
2020
 
        return self.get_juju_output('list-storage-pools', '--format', 'json')
2021
 
 
2022
 
    def create_storage_pool(self, name, provider, size):
2023
 
        """Create storage pool."""
2024
 
        self.juju('create-storage-pool',
2025
 
                  (name, provider,
2026
 
                   'size={}'.format(size)))
2027
 
 
2028
 
    def disable_user(self, user_name):
2029
 
        """Disable an user"""
2030
 
        self.controller_juju('disable-user', (user_name,))
2031
 
 
2032
 
    def enable_user(self, user_name):
2033
 
        """Enable an user"""
2034
 
        self.controller_juju('enable-user', (user_name,))
2035
 
 
2036
 
    def logout(self):
2037
 
        """Logout an user"""
2038
 
        self.controller_juju('logout', ())
2039
 
        self.env.user_name = ''
2040
 
 
2041
 
    def register_user(self, user, juju_home, controller_name=None):
2042
 
        """Register `user` for the `client` return the cloned client used."""
2043
 
        username = user.name
2044
 
        if controller_name is None:
2045
 
            controller_name = '{}_controller'.format(username)
2046
 
 
2047
 
        model = self.env.environment
2048
 
        token = self.add_user_perms(username, models=model,
2049
 
                                    permissions=user.permissions)
2050
 
        user_client = self.create_cloned_environment(juju_home,
2051
 
                                                     controller_name,
2052
 
                                                     username)
2053
 
 
2054
 
        try:
2055
 
            child = user_client.expect('register', (token), include_e=False)
2056
 
            child.expect('(?i)name')
2057
 
            child.sendline(controller_name)
2058
 
            child.expect('(?i)password')
2059
 
            child.sendline(username + '_password')
2060
 
            child.expect('(?i)password')
2061
 
            child.sendline(username + '_password')
2062
 
            child.expect(pexpect.EOF)
2063
 
            if child.isalive():
2064
 
                raise Exception(
2065
 
                    'Registering user failed: pexpect session still alive')
2066
 
        except pexpect.TIMEOUT:
2067
 
            raise Exception(
2068
 
                'Registering user failed: pexpect session timed out')
2069
 
        user_client.env.user_name = username
2070
 
        return user_client
2071
 
 
2072
 
    def remove_user(self, username):
2073
 
        self.juju('remove-user', (username, '-y'), include_e=False)
2074
 
 
2075
 
    def create_cloned_environment(
2076
 
            self, cloned_juju_home, controller_name, user_name=None):
2077
 
        """Create a cloned environment.
2078
 
 
2079
 
        If `user_name` is passed ensures that the cloned environment is updated
2080
 
        to match.
2081
 
 
2082
 
        """
2083
 
        user_client = self.clone(env=self.env.clone())
2084
 
        user_client.env.juju_home = cloned_juju_home
2085
 
        if user_name is not None and user_name != self.env.user_name:
2086
 
            user_client.env.user_name = user_name
2087
 
            user_client.env.environment = qualified_model_name(
2088
 
                user_client.env.environment, self.env.user_name)
2089
 
        # New user names the controller.
2090
 
        user_client.env.controller = Controller(controller_name)
2091
 
        return user_client
2092
 
 
2093
 
    def grant(self, user_name, permission, model=None):
2094
 
        """Grant the user with model or controller permission."""
2095
 
        if permission in self.controller_permissions:
2096
 
            self.juju(
2097
 
                'grant',
2098
 
                (user_name, permission, '-c', self.env.controller.name),
2099
 
                include_e=False)
2100
 
        elif permission in self.model_permissions:
2101
 
            if model is None:
2102
 
                model = self.model_name
2103
 
            self.juju(
2104
 
                'grant',
2105
 
                (user_name, permission, model, '-c', self.env.controller.name),
2106
 
                include_e=False)
2107
 
        else:
2108
 
            raise ValueError('Unknown permission {}'.format(permission))
2109
 
 
2110
 
    def list_clouds(self, format='json'):
2111
 
        """List all the available clouds."""
2112
 
        return self.get_juju_output('list-clouds', '--format',
2113
 
                                    format, include_e=False)
2114
 
 
2115
 
    def show_controller(self, format='json'):
2116
 
        """Show controller's status."""
2117
 
        return self.get_juju_output('show-controller', '--format',
2118
 
                                    format, include_e=False)
2119
 
 
2120
 
    def ssh_keys(self, full=False):
2121
 
        """Give the ssh keys registered for the current model."""
2122
 
        args = []
2123
 
        if full:
2124
 
            args.append('--full')
2125
 
        return self.get_juju_output('ssh-keys', *args)
2126
 
 
2127
 
    def add_ssh_key(self, *keys):
2128
 
        """Add one or more ssh keys to the current model."""
2129
 
        return self.get_juju_output('add-ssh-key', *keys, merge_stderr=True)
2130
 
 
2131
 
    def remove_ssh_key(self, *keys):
2132
 
        """Remove one or more ssh keys from the current model."""
2133
 
        return self.get_juju_output('remove-ssh-key', *keys, merge_stderr=True)
2134
 
 
2135
 
    def import_ssh_key(self, *keys):
2136
 
        """Import ssh keys from one or more identities to the current model."""
2137
 
        return self.get_juju_output('import-ssh-key', *keys, merge_stderr=True)
2138
 
 
2139
 
    def list_disabled_commands(self):
2140
 
        """List all the commands disabled on the model."""
2141
 
        raw = self.get_juju_output('list-disabled-commands',
2142
 
                                   '--format', 'yaml')
2143
 
        return yaml.safe_load(raw)
2144
 
 
2145
 
    def disable_command(self, args):
2146
 
        """Disable a command set."""
2147
 
        return self.juju('disable-command', args)
2148
 
 
2149
 
    def enable_command(self, args):
2150
 
        """Enable a command set."""
2151
 
        return self.juju('enable-command', args)
2152
 
 
2153
 
    def sync_tools(self, local_dir=None):
2154
 
        """Copy tools into a local directory or model."""
2155
 
        if local_dir is None:
2156
 
            return self.juju('sync-tools', ())
2157
 
        else:
2158
 
            return self.juju('sync-tools', ('--local-dir', local_dir),
2159
 
                             include_e=False)
2160
 
 
2161
 
 
2162
 
class EnvJujuClient2B9(EnvJujuClient):
2163
 
 
2164
 
    def update_user_name(self):
2165
 
        return
2166
 
 
2167
 
    def create_cloned_environment(
2168
 
            self, cloned_juju_home, controller_name, user_name=None):
2169
 
        """Create a cloned environment.
2170
 
 
2171
 
        `user_name` is unused in this version of beta.
2172
 
        """
2173
 
        user_client = self.clone(env=self.env.clone())
2174
 
        user_client.env.juju_home = cloned_juju_home
2175
 
        # New user names the controller.
2176
 
        user_client.env.controller = Controller(controller_name)
2177
 
        return user_client
2178
 
 
2179
 
    def get_model_uuid(self):
2180
 
        name = self.env.environment
2181
 
        output_yaml = self.get_juju_output('show-model', '--format', 'yaml')
2182
 
        output = yaml.safe_load(output_yaml)
2183
 
        return output[name]['model-uuid']
2184
 
 
2185
 
    def add_user_perms(self, username, models=None, permissions='read'):
2186
 
        """Adds provided user and return register command arguments.
2187
 
 
2188
 
        :return: Registration token provided by the add-user command.
2189
 
 
2190
 
        """
2191
 
        if models is None:
2192
 
            models = self.env.environment
2193
 
 
2194
 
        args = (username, '--models', models, '--acl', permissions,
2195
 
                '-c', self.env.controller.name)
2196
 
 
2197
 
        output = self.get_juju_output('add-user', *args, include_e=False)
2198
 
        return self._get_register_command(output)
2199
 
 
2200
 
    def grant(self, user_name, permission, model=None):
2201
 
        """Grant the user with a model."""
2202
 
        if model is None:
2203
 
            model = self.model_name
2204
 
        self.juju('grant', (user_name, model, '--acl', permission),
2205
 
                  include_e=False)
2206
 
 
2207
 
    def revoke(self, username, models=None, permissions='read'):
2208
 
        if models is None:
2209
 
            models = self.env.environment
2210
 
 
2211
 
        args = (username, models, '--acl', permissions)
2212
 
 
2213
 
        self.controller_juju('revoke', args)
2214
 
 
2215
 
    def set_config(self, service, options):
2216
 
        option_strings = self._dict_as_option_strings(options)
2217
 
        self.juju('set-config', (service,) + option_strings)
2218
 
 
2219
 
    def get_config(self, service):
2220
 
        return yaml_loads(self.get_juju_output('get-config', service))
2221
 
 
2222
 
    def get_model_config(self):
2223
 
        """Return the value of the environment's configured option."""
2224
 
        return yaml.safe_load(
2225
 
            self.get_juju_output('get-model-config', '--format', 'yaml'))
2226
 
 
2227
 
    def get_env_option(self, option):
2228
 
        """Return the value of the environment's configured option."""
2229
 
        return self.get_juju_output('get-model-config', option)
2230
 
 
2231
 
    def set_env_option(self, option, value):
2232
 
        """Set the value of the option in the environment."""
2233
 
        option_value = "%s=%s" % (option, value)
2234
 
        return self.juju('set-model-config', (option_value,))
2235
 
 
2236
 
    def unset_env_option(self, option):
2237
 
        """Unset the value of the option in the environment."""
2238
 
        return self.juju('unset-model-config', (option,))
2239
 
 
2240
 
    def list_disabled_commands(self):
2241
 
        """List all the commands disabled on the model."""
2242
 
        raw = self.get_juju_output('block list', '--format', 'yaml')
2243
 
        return yaml.safe_load(raw)
2244
 
 
2245
 
    def disable_command(self, args):
2246
 
        """Disable a command set."""
2247
 
        return self.juju('block', args)
2248
 
 
2249
 
    def enable_command(self, args):
2250
 
        """Enable a command set."""
2251
 
        return self.juju('unblock', args)
2252
 
 
2253
 
 
2254
 
class EnvJujuClient2B8(EnvJujuClient2B9):
2255
 
 
2256
 
    status_class = ServiceStatus
2257
 
 
2258
 
    def remove_service(self, service):
2259
 
        self.juju('remove-service', (service,))
2260
 
 
2261
 
    def run(self, commands, applications):
2262
 
        responses = self.get_juju_output(
2263
 
            'run', '--format', 'json', '--service', ','.join(applications),
2264
 
            *commands)
2265
 
        return json.loads(responses)
2266
 
 
2267
 
    def deployer(self, bundle_template, name=None, deploy_delay=10,
2268
 
                 timeout=3600):
2269
 
        """Deploy a bundle using deployer."""
2270
 
        bundle = self.format_bundle(bundle_template)
2271
 
        args = (
2272
 
            '--debug',
2273
 
            '--deploy-delay', str(deploy_delay),
2274
 
            '--timeout', str(timeout),
2275
 
            '--config', bundle,
2276
 
        )
2277
 
        if name:
2278
 
            args += (name,)
2279
 
        e_arg = ('-e', 'local.{}:{}'.format(
2280
 
            self.env.controller.name, self.env.environment))
2281
 
        args = e_arg + args
2282
 
        self.juju('deployer', args, self.env.needs_sudo(), include_e=False)
2283
 
 
2284
 
 
2285
 
class EnvJujuClient2B7(EnvJujuClient2B8):
2286
 
 
2287
 
    def get_controller_model_name(self):
2288
 
        """Return the name of the 'controller' model.
2289
 
 
2290
 
        Return the name of the environment when an 'controller' model does
2291
 
        not exist.
2292
 
        """
2293
 
        return 'admin'
2294
 
 
2295
 
 
2296
 
class EnvJujuClient2B3(EnvJujuClient2B7):
2297
 
 
2298
 
    def _add_model(self, model_name, config_file):
2299
 
        self.controller_juju('create-model', (
2300
 
            model_name, '--config', config_file))
2301
 
 
2302
 
 
2303
 
class EnvJujuClient2B2(EnvJujuClient2B3):
2304
 
 
2305
 
    def get_bootstrap_args(
2306
 
            self, upload_tools, config_filename, bootstrap_series=None,
2307
 
            credential=None, auto_upgrade=False, metadata_source=None,
2308
 
            to=None, agent_version=None):
2309
 
        """Return the bootstrap arguments for the substrate."""
2310
 
        err_fmt = 'EnvJujuClient2B2 does not support bootstrap argument {}'
2311
 
        if auto_upgrade:
2312
 
            raise ValueError(err_fmt.format('auto_upgrade'))
2313
 
        if metadata_source is not None:
2314
 
            raise ValueError(err_fmt.format('metadata_source'))
2315
 
        if to is not None:
2316
 
            raise ValueError(err_fmt.format('to'))
2317
 
        if agent_version is not None:
2318
 
            raise ValueError(err_fmt.format('agent_version'))
2319
 
        if self.env.joyent:
2320
 
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
2321
 
            constraints = 'mem=2G cpu-cores=1'
2322
 
        else:
2323
 
            constraints = 'mem=2G'
2324
 
        cloud_region = self.get_cloud_region(self.env.get_cloud(),
2325
 
                                             self.env.get_region())
2326
 
        args = ['--constraints', constraints, self.env.environment,
2327
 
                cloud_region, '--config', config_filename]
2328
 
        if upload_tools:
2329
 
            args.insert(0, '--upload-tools')
2330
 
        else:
2331
 
            args.extend(['--agent-version', self.get_matching_agent_version()])
2332
 
 
2333
 
        if bootstrap_series is not None:
2334
 
            args.extend(['--bootstrap-series', bootstrap_series])
2335
 
 
2336
 
        if credential is not None:
2337
 
            args.extend(['--credential', credential])
2338
 
 
2339
 
        return tuple(args)
2340
 
 
2341
 
    def get_controller_client(self):
2342
 
        """Return a client for the controller model.  May return self."""
2343
 
        return self
2344
 
 
2345
 
    def get_controller_model_name(self):
2346
 
        """Return the name of the 'controller' model.
2347
 
 
2348
 
        Return the name of the environment when an 'controller' model does
2349
 
        not exist.
2350
 
        """
2351
 
        models = self.get_models()
2352
 
        # The dict can be empty because 1.x does not support the models.
2353
 
        # This is an ambiguous case for the jes feature flag which supports
2354
 
        # multiple models, but none is named 'admin' by default. Since the
2355
 
        # jes case also uses '-e' for models, the env is the controller model.
2356
 
        for model in models.get('models', []):
2357
 
            if 'admin' in model['name']:
2358
 
                return 'admin'
2359
 
        return self.env.environment
2360
 
 
2361
 
 
2362
 
class EnvJujuClient2A2(EnvJujuClient2B2):
2363
 
    """Drives Juju 2.0-alpha2 clients."""
2364
 
 
2365
 
    default_backend = Juju2A2Backend
2366
 
 
2367
 
    config_class = SimpleEnvironment
2368
 
 
2369
 
    @classmethod
2370
 
    def _get_env(cls, env):
2371
 
        if isinstance(env, JujuData):
2372
 
            raise IncompatibleConfigClass(
2373
 
                'JujuData cannot be used with {}'.format(cls.__name__))
2374
 
        return env
2375
 
 
2376
 
    def bootstrap(self, upload_tools=False, bootstrap_series=None):
2377
 
        """Bootstrap a controller."""
2378
 
        self._check_bootstrap()
2379
 
        args = self.get_bootstrap_args(upload_tools, bootstrap_series)
2380
 
        self.juju('bootstrap', args, self.env.needs_sudo())
2381
 
 
2382
 
    @contextmanager
2383
 
    def bootstrap_async(self, upload_tools=False):
2384
 
        self._check_bootstrap()
2385
 
        args = self.get_bootstrap_args(upload_tools)
2386
 
        with self.juju_async('bootstrap', args):
2387
 
            yield
2388
 
            log.info('Waiting for bootstrap of {}.'.format(
2389
 
                self.env.environment))
2390
 
 
2391
 
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2392
 
                           credential=None):
2393
 
        """Return the bootstrap arguments for the substrate."""
2394
 
        if credential is not None:
2395
 
            raise ValueError(
2396
 
                '--credential is not supported by this juju version.')
2397
 
        constraints = self._get_substrate_constraints()
2398
 
        args = ('--constraints', constraints,
2399
 
                '--agent-version', self.get_matching_agent_version())
2400
 
        if upload_tools:
2401
 
            args = ('--upload-tools',) + args
2402
 
        if bootstrap_series is not None:
2403
 
            args = args + ('--bootstrap-series', bootstrap_series)
2404
 
        return args
2405
 
 
2406
 
    def deploy(self, charm, repository=None, to=None, series=None,
2407
 
               service=None, force=False, storage=None, constraints=None):
2408
 
        args = [charm]
2409
 
        if repository is not None:
2410
 
            args.extend(['--repository', repository])
2411
 
        if to is not None:
2412
 
            args.extend(['--to', to])
2413
 
        if service is not None:
2414
 
            args.extend([service])
2415
 
        if storage is not None:
2416
 
            args.extend(['--storage', storage])
2417
 
        if constraints is not None:
2418
 
            args.extend(['--constraints', constraints])
2419
 
        return self.juju('deploy', tuple(args))
2420
 
 
2421
 
 
2422
 
class EnvJujuClient2A1(EnvJujuClient2A2):
2423
 
    """Drives Juju 2.0-alpha1 clients."""
2424
 
 
2425
 
    _show_status = 'status'
2426
 
 
2427
 
    default_backend = Juju1XBackend
2428
 
 
2429
 
    def get_cache_path(self):
2430
 
        return get_cache_path(self.env.juju_home, models=False)
2431
 
 
2432
 
    def remove_service(self, service):
2433
 
        self.juju('destroy-service', (service,))
2434
 
 
2435
706
    def backup(self):
2436
707
        environ = self._shell_environ()
2437
708
        # juju-backup does not support the -e flag.
2440
711
            # Mutate os.environ instead of supplying env parameter so Windows
2441
712
            # can search env['PATH']
2442
713
            with scoped_environ(environ):
2443
 
                args = ['juju', 'backup']
2444
 
                log.info(' '.join(args))
2445
 
                output = subprocess.check_output(args)
 
714
                output = subprocess.check_output(['juju', 'backup'])
2446
715
        except subprocess.CalledProcessError as e:
2447
716
            log.info(e.output)
2448
717
            raise
2457
726
        log.info("State-Server backup at %s", backup_file_path)
2458
727
        return backup_file_path
2459
728
 
2460
 
    def restore_backup(self, backup_file):
2461
 
        return self.get_juju_output('restore', '--constraints', 'mem=2G',
2462
 
                                    backup_file)
2463
 
 
2464
 
    def restore_backup_async(self, backup_file):
2465
 
        return self.juju_async('restore', ('--constraints', 'mem=2G',
2466
 
                                           backup_file))
2467
 
 
2468
 
    def enable_ha(self):
2469
 
        self.juju('ensure-availability', ('-n', '3'))
2470
 
 
2471
 
    def list_models(self):
2472
 
        """List the models registered with the current controller."""
2473
 
        log.info('The model is environment {}'.format(self.env.environment))
2474
 
 
2475
 
    def list_clouds(self, format='json'):
2476
 
        """List all the available clouds."""
2477
 
        return {}
2478
 
 
2479
 
    def show_controller(self, format='json'):
2480
 
        """Show controller's status."""
2481
 
        return {}
2482
 
 
2483
 
    def get_models(self):
2484
 
        """return a models dict with a 'models': [] key-value pair."""
2485
 
        return {}
2486
 
 
2487
 
    def _get_models(self):
2488
 
        """return a list of model dicts."""
2489
 
        # In 2.0-alpha1, 'list-models' produced a yaml list rather than a
2490
 
        # dict, but the command and parsing are the same.
2491
 
        return super(EnvJujuClient2A1, self).get_models()
2492
 
 
2493
 
    def list_controllers(self):
2494
 
        """List the controllers."""
2495
 
        log.info(
2496
 
            'The controller is environment {}'.format(self.env.environment))
2497
 
 
2498
 
    @staticmethod
2499
 
    def get_controller_member_status(info_dict):
2500
 
        return info_dict.get('state-server-member-status')
2501
 
 
2502
729
    def action_fetch(self, id, action=None, timeout="1m"):
2503
730
        """Fetches the results of the action with the given id.
2504
731
 
2539
766
                            output)
2540
767
        return match.group(1)
2541
768
 
2542
 
    def list_space(self):
2543
 
        return yaml.safe_load(self.get_juju_output('space list'))
2544
 
 
2545
 
    def add_space(self, space):
2546
 
        self.juju('space create', (space),)
2547
 
 
2548
 
    def add_subnet(self, subnet, space):
2549
 
        self.juju('subnet add', (subnet, space))
2550
 
 
2551
 
    def set_model_constraints(self, constraints):
2552
 
        constraint_strings = self._dict_as_option_strings(constraints)
2553
 
        return self.juju('set-constraints', constraint_strings)
2554
 
 
2555
 
    def set_config(self, service, options):
2556
 
        option_strings = ['{}={}'.format(*item) for item in options.items()]
2557
 
        self.juju('set', (service,) + tuple(option_strings))
2558
 
 
2559
 
    def get_config(self, service):
2560
 
        return yaml_loads(self.get_juju_output('get', service))
2561
 
 
2562
 
    def get_model_config(self):
2563
 
        """Return the value of the environment's configured option."""
2564
 
        return yaml.safe_load(self.get_juju_output('get-env'))
2565
 
 
2566
 
    def get_env_option(self, option):
2567
 
        """Return the value of the environment's configured option."""
2568
 
        return self.get_juju_output('get-env', option)
2569
 
 
2570
 
    def set_env_option(self, option, value):
2571
 
        """Set the value of the option in the environment."""
2572
 
        option_value = "%s=%s" % (option, value)
2573
 
        return self.juju('set-env', (option_value,))
2574
 
 
2575
 
 
2576
 
class EnvJujuClient1X(EnvJujuClient2A1):
2577
 
    """Base for all 1.x client drivers."""
2578
 
 
2579
 
    # The environments.yaml options that are replaced by bootstrap options.
2580
 
    # For Juju 1.x, no bootstrap options are used.
2581
 
    bootstrap_replaces = frozenset()
2582
 
 
2583
 
    destroy_model_command = 'destroy-environment'
2584
 
 
2585
 
    supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE])
2586
 
 
2587
 
    agent_metadata_url = 'tools-metadata-url'
2588
 
 
2589
 
    def _cmd_model(self, include_e, controller):
2590
 
        if controller:
2591
 
            return self.get_controller_model_name()
2592
 
        elif self.env is None or not include_e:
2593
 
            return None
2594
 
        else:
2595
 
            return unqualified_model_name(self.model_name)
2596
 
 
2597
 
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2598
 
                           credential=None):
2599
 
        """Return the bootstrap arguments for the substrate."""
2600
 
        if credential is not None:
2601
 
            raise ValueError(
2602
 
                '--credential is not supported by this juju version.')
2603
 
        constraints = self._get_substrate_constraints()
2604
 
        args = ('--constraints', constraints)
2605
 
        if upload_tools:
2606
 
            args = ('--upload-tools',) + args
2607
 
        if bootstrap_series is not None:
2608
 
            env_val = self.env.config.get('default-series')
2609
 
            if bootstrap_series != env_val:
2610
 
                raise BootstrapMismatch(
2611
 
                    'bootstrap-series', bootstrap_series, 'default-series',
2612
 
                    env_val)
2613
 
        return args
2614
 
 
2615
 
    def get_jes_command(self):
2616
 
        """Return the JES command to destroy a controller.
2617
 
 
2618
 
        Juju 2.x has 'kill-controller'.
2619
 
        Some intermediate versions had 'controller kill'.
2620
 
        Juju 1.25 has 'system kill' when the jes feature flag is set.
2621
 
 
2622
 
        :raises: JESNotSupported when the version of Juju does not expose
2623
 
            a JES command.
2624
 
        :return: The JES command.
2625
 
        """
2626
 
        commands = self.get_juju_output('help', 'commands', include_e=False)
2627
 
        for line in commands.splitlines():
2628
 
            for cmd in _jes_cmds.keys():
2629
 
                if line.startswith(cmd):
2630
 
                    return cmd
2631
 
        raise JESNotSupported()
2632
 
 
2633
 
    def upgrade_juju(self, force_version=True):
2634
 
        args = ()
2635
 
        if force_version:
2636
 
            version = self.get_matching_agent_version(no_build=True)
2637
 
            args += ('--version', version)
2638
 
        if self.env.local:
2639
 
            args += ('--upload-tools',)
2640
 
        self._upgrade_juju(args)
2641
 
 
2642
 
    def make_model_config(self):
2643
 
        config_dict = make_safe_config(self)
2644
 
        # Strip unneeded variables.
2645
 
        return config_dict
2646
 
 
2647
 
    def _add_model(self, model_name, config_file):
2648
 
        seen_cmd = self.get_jes_command()
2649
 
        if seen_cmd == SYSTEM:
2650
 
            controller_option = ('-s', self.env.environment)
2651
 
        else:
2652
 
            controller_option = ('-c', self.env.environment)
2653
 
        self.juju(_jes_cmds[seen_cmd]['create'], controller_option + (
2654
 
            model_name, '--config', config_file), include_e=False)
2655
 
 
2656
 
    def destroy_model(self):
2657
 
        """With JES enabled, destroy-environment destroys the model."""
2658
 
        self.destroy_environment(force=False)
2659
 
 
2660
 
    def destroy_environment(self, force=True, delete_jenv=False):
2661
 
        if force:
2662
 
            force_arg = ('--force',)
2663
 
        else:
2664
 
            force_arg = ()
2665
 
        exit_status = self.juju(
2666
 
            'destroy-environment',
2667
 
            (self.env.environment,) + force_arg + ('-y',),
2668
 
            self.env.needs_sudo(), check=False, include_e=False,
2669
 
            timeout=get_teardown_timeout(self))
2670
 
        if delete_jenv:
2671
 
            jenv_path = get_jenv_path(self.env.juju_home, self.env.environment)
2672
 
            ensure_deleted(jenv_path)
2673
 
        return exit_status
2674
 
 
2675
 
    def _get_models(self):
2676
 
        """return a list of model dicts."""
2677
 
        try:
2678
 
            return yaml.safe_load(self.get_juju_output(
2679
 
                'environments', '-s', self.env.environment, '--format', 'yaml',
2680
 
                include_e=False))
2681
 
        except subprocess.CalledProcessError:
2682
 
            # This *private* method attempts to use a 1.25 JES feature.
2683
 
            # The JES design is dead. The private method is not used to
2684
 
            # directly test juju cli; the failure is not a contract violation.
2685
 
            log.info('Call to JES juju environments failed, falling back.')
2686
 
            return []
2687
 
 
2688
 
    def deploy_bundle(self, bundle, timeout=_DEFAULT_BUNDLE_TIMEOUT):
2689
 
        """Deploy bundle using deployer for Juju 1.X version."""
2690
 
        self.deployer(bundle, timeout=timeout)
2691
 
 
2692
 
    def deployer(self, bundle_template, name=None, deploy_delay=10,
2693
 
                 timeout=3600):
2694
 
        """Deploy a bundle using deployer."""
2695
 
        bundle = self.format_bundle(bundle_template)
2696
 
        args = (
2697
 
            '--debug',
2698
 
            '--deploy-delay', str(deploy_delay),
2699
 
            '--timeout', str(timeout),
2700
 
            '--config', bundle,
2701
 
        )
2702
 
        if name:
2703
 
            args += (name,)
2704
 
        self.juju('deployer', args, self.env.needs_sudo())
2705
 
 
2706
 
    def upgrade_charm(self, service, charm_path=None):
2707
 
        args = (service,)
2708
 
        if charm_path is not None:
2709
 
            repository = os.path.dirname(os.path.dirname(charm_path))
2710
 
            args = args + ('--repository', repository)
2711
 
        self.juju('upgrade-charm', args)
2712
 
 
2713
 
    def get_controller_endpoint(self):
2714
 
        """Return the address of the state-server leader."""
2715
 
        endpoint = self.get_juju_output('api-endpoints')
2716
 
        address, port = split_address_port(endpoint)
2717
 
        return address
2718
 
 
2719
 
    def upgrade_mongo(self):
2720
 
        raise UpgradeMongoNotSupported()
2721
 
 
2722
 
    def add_storage(self, unit, storage_type, amount="1"):
2723
 
        """Add storage instances to service.
2724
 
 
2725
 
        Only type 'disk' is able to add instances.
2726
 
        """
2727
 
        self.juju('storage add', (unit, storage_type + "=" + amount))
2728
 
 
2729
 
    def list_storage(self):
2730
 
        """Return the storage list."""
2731
 
        return self.get_juju_output('storage list', '--format', 'json')
2732
 
 
2733
 
    def list_storage_pool(self):
2734
 
        """Return the list of storage pool."""
2735
 
        return self.get_juju_output('storage pool list', '--format', 'json')
2736
 
 
2737
 
    def create_storage_pool(self, name, provider, size):
2738
 
        """Create storage pool."""
2739
 
        self.juju('storage pool create',
2740
 
                  (name, provider,
2741
 
                   'size={}'.format(size)))
2742
 
 
2743
 
    def ssh_keys(self, full=False):
2744
 
        """Give the ssh keys registered for the current model."""
2745
 
        args = []
2746
 
        if full:
2747
 
            args.append('--full')
2748
 
        return self.get_juju_output('authorized-keys list', *args)
2749
 
 
2750
 
    def add_ssh_key(self, *keys):
2751
 
        """Add one or more ssh keys to the current model."""
2752
 
        return self.get_juju_output('authorized-keys add', *keys,
2753
 
                                    merge_stderr=True)
2754
 
 
2755
 
    def remove_ssh_key(self, *keys):
2756
 
        """Remove one or more ssh keys from the current model."""
2757
 
        return self.get_juju_output('authorized-keys delete', *keys,
2758
 
                                    merge_stderr=True)
2759
 
 
2760
 
    def import_ssh_key(self, *keys):
2761
 
        """Import ssh keys from one or more identities to the current model."""
2762
 
        return self.get_juju_output('authorized-keys import', *keys,
2763
 
                                    merge_stderr=True)
2764
 
 
2765
 
 
2766
 
class EnvJujuClient22(EnvJujuClient1X):
2767
 
 
2768
 
    used_feature_flags = frozenset(['actions'])
2769
 
 
2770
 
    def __init__(self, *args, **kwargs):
2771
 
        super(EnvJujuClient22, self).__init__(*args, **kwargs)
2772
 
        self.feature_flags.add('actions')
2773
 
 
2774
 
 
2775
 
class EnvJujuClient26(EnvJujuClient1X):
 
769
    def action_do_fetch(self, unit, action, timeout="1m", *args):
 
770
        """Performs given action on given unit and waits for the results.
 
771
 
 
772
        Action params should be given as args in the form foo=bar.
 
773
        Returns the yaml output of the action.
 
774
        """
 
775
        id = self.action_do(unit, action, *args)
 
776
        return self.action_fetch(id, action, timeout)
 
777
 
 
778
 
 
779
class EnvJujuClient22(EnvJujuClient):
 
780
 
 
781
    def _shell_environ(self):
 
782
        """Generate a suitable shell environment.
 
783
 
 
784
        Juju's directory must be in the PATH to support plugins.
 
785
        """
 
786
        env = super(EnvJujuClient22, self)._shell_environ()
 
787
        env[JUJU_DEV_FEATURE_FLAGS] = 'actions'
 
788
        return env
 
789
 
 
790
 
 
791
class EnvJujuClient26(EnvJujuClient):
2776
792
    """Drives Juju 2.6-series clients."""
2777
793
 
2778
 
    used_feature_flags = frozenset(['address-allocation', 'cloudsigma', 'jes'])
2779
 
 
2780
794
    def __init__(self, *args, **kwargs):
2781
795
        super(EnvJujuClient26, self).__init__(*args, **kwargs)
2782
 
        if self.env is None or self.env.config is None:
2783
 
            return
2784
 
        if self.env.config.get('type') == 'cloudsigma':
2785
 
            self.feature_flags.add('cloudsigma')
 
796
        self._use_jes = False
 
797
        self._use_container_address_allocation = False
2786
798
 
2787
799
    def enable_jes(self):
2788
800
        """Enable JES if JES is optional.
2792
804
        :raises: JESNotSupported when JES is not supported; Juju does not have
2793
805
            the 'system kill' command when the JES feature flag is set.
2794
806
        """
2795
 
 
2796
 
        if 'jes' in self.feature_flags:
 
807
        if self._use_jes:
2797
808
            return
2798
809
        if self.is_jes_enabled():
2799
810
            raise JESByDefault()
2800
 
        self.feature_flags.add('jes')
 
811
        self._use_jes = True
2801
812
        if not self.is_jes_enabled():
2802
 
            self.feature_flags.remove('jes')
 
813
            self._use_jes = False
2803
814
            raise JESNotSupported()
2804
815
 
2805
816
    def disable_jes(self):
2806
 
        if 'jes' in self.feature_flags:
2807
 
            self.feature_flags.remove('jes')
 
817
        self._use_jes = False
2808
818
 
2809
819
    def enable_container_address_allocation(self):
2810
 
        self.feature_flags.add('address-allocation')
 
820
        self._use_container_address_allocation = True
 
821
 
 
822
    def _get_feature_flags(self):
 
823
        if self.env.config.get('type') == 'cloudsigma':
 
824
            yield 'cloudsigma'
 
825
        if self._use_jes is True:
 
826
            yield 'jes'
 
827
        if self._use_container_address_allocation:
 
828
            yield 'address-allocation'
 
829
 
 
830
    def _shell_environ(self):
 
831
        """Generate a suitable shell environment.
 
832
 
 
833
        Juju's directory must be in the PATH to support plugins.
 
834
        """
 
835
        env = super(EnvJujuClient26, self)._shell_environ()
 
836
        feature_flags = self._get_feature_flags()
 
837
        env[JUJU_DEV_FEATURE_FLAGS] = ','.join(feature_flags)
 
838
        return env
2811
839
 
2812
840
 
2813
841
class EnvJujuClient25(EnvJujuClient26):
2817
845
class EnvJujuClient24(EnvJujuClient25):
2818
846
    """Similar to EnvJujuClient25, but lacking JES support."""
2819
847
 
2820
 
    used_feature_flags = frozenset(['cloudsigma'])
2821
 
 
2822
848
    def enable_jes(self):
2823
849
        raise JESNotSupported()
2824
850
 
2825
 
    def add_ssh_machines(self, machines):
2826
 
        for machine in machines:
2827
 
            self.juju('add-machine', ('ssh:' + machine,))
 
851
    def _get_feature_flags(self):
 
852
        if self.env.config.get('type') == 'cloudsigma':
 
853
            yield 'cloudsigma'
2828
854
 
2829
855
 
2830
856
def get_local_root(juju_home, env):
2903
929
 
2904
930
 
2905
931
def dump_environments_yaml(juju_home, config):
2906
 
    """Dump yaml data to the environment file.
2907
 
 
2908
 
    :param juju_home: Path to the JUJU_HOME directory.
2909
 
    :param config: Dictionary repersenting yaml data to dump."""
2910
932
    environments_path = get_environments_path(juju_home)
2911
933
    with open(environments_path, 'w') as config_file:
2912
934
        yaml.safe_dump(config, config_file)
2928
950
        with context:
2929
951
            if set_home:
2930
952
                os.environ['JUJU_HOME'] = temp_juju_home
2931
 
                os.environ['JUJU_DATA'] = temp_juju_home
2932
953
            yield temp_juju_home
2933
954
 
2934
955
 
2936
957
    return os.path.join(juju_home, 'jes-homes', dir_name)
2937
958
 
2938
959
 
2939
 
def get_cache_path(juju_home, models=False):
2940
 
    if models:
2941
 
        root = os.path.join(juju_home, 'models')
2942
 
    else:
2943
 
        root = os.path.join(juju_home, 'environments')
2944
 
    return os.path.join(root, 'cache.yaml')
 
960
def get_cache_path(juju_home):
 
961
    return os.path.join(juju_home, 'environments', 'cache.yaml')
 
962
 
 
963
 
 
964
@contextmanager
 
965
def make_jes_home(juju_home, dir_name, config):
 
966
    home_path = jes_home_path(juju_home, dir_name)
 
967
    if os.path.exists(home_path):
 
968
        rmtree(home_path)
 
969
    os.makedirs(home_path)
 
970
    dump_environments_yaml(home_path, config)
 
971
    yield home_path
2945
972
 
2946
973
 
2947
974
def make_safe_config(client):
2948
975
    config = dict(client.env.config)
2949
 
    if 'agent-version' in client.bootstrap_replaces:
2950
 
        config.pop('agent-version', None)
2951
 
    else:
2952
 
        config['agent-version'] = client.get_matching_agent_version()
 
976
    config['agent-version'] = client.get_matching_agent_version()
2953
977
    # AFAICT, we *always* want to set test-mode to True.  If we ever find a
2954
978
    # use-case where we don't, we can make this optional.
2955
979
    config['test-mode'] = True
2956
980
    # Explicitly set 'name', which Juju implicitly sets to env.environment to
2957
981
    # ensure MAASAccount knows what the name will be.
2958
 
    config['name'] = unqualified_model_name(client.env.environment)
 
982
    config['name'] = client.env.environment
2959
983
    if config['type'] == 'local':
2960
984
        config.setdefault('root-dir', get_local_root(client.env.juju_home,
2961
985
                          client.env))
2991
1015
    # Always bootstrap a matching environment.
2992
1016
    jenv_path = get_jenv_path(juju_home, client.env.environment)
2993
1017
    if permanent:
2994
 
        context = client.env.make_jes_home(
2995
 
            juju_home, client.env.environment, new_config)
 
1018
        context = make_jes_home(juju_home, client.env.environment, new_config)
2996
1019
    else:
2997
1020
        context = _temp_env(new_config, juju_home, set_home)
2998
1021
    with context as temp_juju_home:
3043
1066
    return host
3044
1067
 
3045
1068
 
3046
 
class Controller:
3047
 
    """Represents the controller for a model or models."""
3048
 
 
3049
 
    def __init__(self, name):
3050
 
        self.name = name
 
1069
class Status:
 
1070
 
 
1071
    def __init__(self, status, status_text):
 
1072
        self.status = status
 
1073
        self.status_text = status_text
 
1074
 
 
1075
    @classmethod
 
1076
    def from_text(cls, text):
 
1077
        status_yaml = yaml_loads(text)
 
1078
        return cls(status_yaml, text)
 
1079
 
 
1080
    def iter_machines(self, containers=False, machines=True):
 
1081
        for machine_name, machine in sorted(self.status['machines'].items()):
 
1082
            if machines:
 
1083
                yield machine_name, machine
 
1084
            if containers:
 
1085
                for contained, unit in machine.get('containers', {}).items():
 
1086
                    yield contained, unit
 
1087
 
 
1088
    def iter_new_machines(self, old_status):
 
1089
        for machine, data in self.iter_machines():
 
1090
            if machine in old_status.status['machines']:
 
1091
                continue
 
1092
            yield machine, data
 
1093
 
 
1094
    def iter_units(self):
 
1095
        for service_name, service in sorted(self.status['services'].items()):
 
1096
            for unit_name, unit in sorted(service.get('units', {}).items()):
 
1097
                yield unit_name, unit
 
1098
                subordinates = unit.get('subordinates', ())
 
1099
                for sub_name in sorted(subordinates):
 
1100
                    yield sub_name, subordinates[sub_name]
 
1101
 
 
1102
    def agent_items(self):
 
1103
        for machine_name, machine in self.iter_machines(containers=True):
 
1104
            yield machine_name, machine
 
1105
        for unit_name, unit in self.iter_units():
 
1106
            yield unit_name, unit
 
1107
 
 
1108
    def agent_states(self):
 
1109
        """Map agent states to the units and machines in those states."""
 
1110
        states = defaultdict(list)
 
1111
        for item_name, item in self.agent_items():
 
1112
            states[item.get('agent-state', 'no-agent')].append(item_name)
 
1113
        return states
 
1114
 
 
1115
    def check_agents_started(self, environment_name=None):
 
1116
        """Check whether all agents are in the 'started' state.
 
1117
 
 
1118
        If not, return agent_states output.  If so, return None.
 
1119
        If an error is encountered for an agent, raise ErroredUnit
 
1120
        """
 
1121
        bad_state_info = re.compile(
 
1122
            '(.*error|^(cannot set up groups|cannot run instance)).*')
 
1123
        for item_name, item in self.agent_items():
 
1124
            state_info = item.get('agent-state-info', '')
 
1125
            if bad_state_info.match(state_info):
 
1126
                raise ErroredUnit(item_name, state_info)
 
1127
        states = self.agent_states()
 
1128
        if states.keys() == ['started']:
 
1129
            return None
 
1130
        for state, entries in states.items():
 
1131
            if 'error' in state:
 
1132
                raise ErroredUnit(entries[0], state)
 
1133
        return states
 
1134
 
 
1135
    def get_service_count(self):
 
1136
        return len(self.status.get('services', {}))
 
1137
 
 
1138
    def get_service_unit_count(self, service):
 
1139
        return len(
 
1140
            self.status.get('services', {}).get(service, {}).get('units', {}))
 
1141
 
 
1142
    def get_agent_versions(self):
 
1143
        versions = defaultdict(set)
 
1144
        for item_name, item in self.agent_items():
 
1145
            versions[item.get('agent-version', 'unknown')].add(item_name)
 
1146
        return versions
 
1147
 
 
1148
    def get_instance_id(self, machine_id):
 
1149
        return self.status['machines'][machine_id]['instance-id']
 
1150
 
 
1151
    def get_unit(self, unit_name):
 
1152
        """Return metadata about a unit."""
 
1153
        for service in sorted(self.status['services'].values()):
 
1154
            if unit_name in service.get('units', {}):
 
1155
                return service['units'][unit_name]
 
1156
        raise KeyError(unit_name)
 
1157
 
 
1158
    def service_subordinate_units(self, service_name):
 
1159
        """Return subordinate metadata for a service_name."""
 
1160
        services = self.status.get('services', {})
 
1161
        if service_name in services:
 
1162
            for unit in sorted(services[service_name].get(
 
1163
                    'units', {}).values()):
 
1164
                for sub_name, sub in unit.get('subordinates', {}).items():
 
1165
                    yield sub_name, sub
 
1166
 
 
1167
    def get_open_ports(self, unit_name):
 
1168
        """List the open ports for the specified unit.
 
1169
 
 
1170
        If no ports are listed for the unit, the empty list is returned.
 
1171
        """
 
1172
        return self.get_unit(unit_name).get('open-ports', [])
 
1173
 
 
1174
 
 
1175
class SimpleEnvironment:
 
1176
 
 
1177
    def __init__(self, environment, config=None, juju_home=None):
 
1178
        self.environment = environment
 
1179
        self.config = config
 
1180
        self.juju_home = juju_home
 
1181
        if self.config is not None:
 
1182
            self.local = bool(self.config.get('type') == 'local')
 
1183
            self.kvm = (
 
1184
                self.local and bool(self.config.get('container') == 'kvm'))
 
1185
            self.hpcloud = bool(
 
1186
                'hpcloudsvc' in self.config.get('auth-url', ''))
 
1187
            self.maas = bool(self.config.get('type') == 'maas')
 
1188
            self.joyent = bool(self.config.get('type') == 'joyent')
 
1189
        else:
 
1190
            self.local = False
 
1191
            self.kvm = False
 
1192
            self.hpcloud = False
 
1193
            self.maas = False
 
1194
            self.joyent = False
 
1195
 
 
1196
    def __eq__(self, other):
 
1197
        if type(self) != type(other):
 
1198
            return False
 
1199
        if self.environment != other.environment:
 
1200
            return False
 
1201
        if self.config != other.config:
 
1202
            return False
 
1203
        if self.local != other.local:
 
1204
            return False
 
1205
        if self.hpcloud != other.hpcloud:
 
1206
            return False
 
1207
        if self.maas != other.maas:
 
1208
            return False
 
1209
        return True
 
1210
 
 
1211
    def __ne__(self, other):
 
1212
        return not self == other
 
1213
 
 
1214
    @classmethod
 
1215
    def from_config(cls, name):
 
1216
        config, selected = get_selected_environment(name)
 
1217
        if name is None:
 
1218
            name = selected
 
1219
        return cls(name, config)
 
1220
 
 
1221
    def needs_sudo(self):
 
1222
        return self.local
3051
1223
 
3052
1224
 
3053
1225
class GroupReporter: