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

161 by Curtis Hovey
Windows and py3 compatability.
1
from __future__ import print_function
2
1306.1.1 by Curtis Hovey
Save spike to get leader and followers.
3
from collections import (
4
    defaultdict,
5
    namedtuple,
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
6
    )
807.1.1 by Aaron Bentley
Avoid creating temp juju homes inside temp juju homes.
7
from contextlib import (
8
    contextmanager,
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
9
    )
1315.2.3 by Aaron Bentley
Use admin client to get bootstrap host.
10
from copy import deepcopy
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
11
from datetime import (
12
    datetime,
13
    timedelta,
14
    )
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
15
import errno
751.1.1 by Aaron Bentley
Give equal time to old and new clients in DeployManyAttempt
16
from itertools import chain
1437.1.1 by Aaron Bentley
Speed up tests by using JSON for status.
17
import json
715.2.1 by John George
add debug logging option to show command and output from juju calls made from get_juju_output
18
import logging
161 by Curtis Hovey
Windows and py3 compatability.
19
import os
717.2.1 by Aaron Bentley
Extract EnvJujuClient.backup from assess_recovery.
20
import re
1695.2.12 by Andrew Beach
Fixes for more comments.
21
import shutil
2 by Aaron Bentley
Added initial deploy_stack.
22
import subprocess
23
import sys
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
24
from tempfile import NamedTemporaryFile
1048.2.1 by John George
Capture duration of juju commands from the client perspective.
25
import time
2 by Aaron Bentley
Added initial deploy_stack.
26
1703.1.8 by Andrew Beach
Added test labels.
27
import pexpect
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
28
import yaml
29
30
from jujuconfig import (
31
    get_environments_path,
32
    get_jenv_path,
777.2.1 by Aaron Bentley
Always delete jenv in industrial test.
33
    get_juju_home,
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
34
    get_selected_environment,
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
35
    )
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
36
from utility import (
37
    check_free_disk_space,
777.2.1 by Aaron Bentley
Always delete jenv in industrial test.
38
    ensure_deleted,
1091.4.1 by James Tunnicliffe
Merged upstream
39
    ensure_dir,
1167.2.1 by Martin Packman
Support IPv6 in wait_for_port and move handling of literal addresses closer to final use
40
    is_ipv6_address,
1417.1.1 by Seman
Added more resource CI tests.
41
    JujuResourceTimeout,
763.1.2 by Curtis Hovey
Added puase to sleep after ha, but remove it for tests.
42
    pause,
1401.2.12 by Christopher Lee
Safer command quoting for expect().
43
    quote,
1477.3.1 by Andrew Wilkins
beebop
44
    qualified_model_name,
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
45
    scoped_environ,
1703.1.6 by Andrew Beach
Renamed the function to skip_on_missing_file.
46
    skip_on_missing_file,
1310.1.2 by Curtis Hovey
Address ipv6 split address and port.
47
    split_address_port,
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
48
    temp_dir,
1477.3.1 by Andrew Wilkins
beebop
49
    unqualified_model_name,
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
50
    until_timeout,
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
51
    )
112.1.4 by Aaron Bentley
Read config, use it to determine whether provider is local.
52
2 by Aaron Bentley
Added initial deploy_stack.
53
1092.2.2 by Aaron Bentley
Fix lint.
54
__metaclass__ = type
55
1246.1.2 by Curtis Hovey
Added coalesce_agent_status to handle machine agent-state and unit agent-status.
56
AGENTS_READY = set(['started', 'idle'])
163 by Curtis Hovey
Extracted the windows command incase it needs to be reused.
57
WIN_JUJU_CMD = os.path.join('\\', 'Progra~2', 'Juju', 'juju.exe')
58
928.2.2 by Aaron Bentley
Auto-enable support for cloudsigma in 1.24.
59
JUJU_DEV_FEATURE_FLAGS = 'JUJU_DEV_FEATURE_FLAGS'
1242.2.1 by Aaron Bentley
EnvJujuClient assumes JES enabled, since it assumes 2.0.
60
CONTROLLER = 'controller'
61
KILL_CONTROLLER = 'kill-controller'
62
SYSTEM = 'system'
928.2.2 by Aaron Bentley
Auto-enable support for cloudsigma in 1.24.
63
1341.2.1 by Aaron Bentley
Move container support knowledge to jujupy.
64
KVM_MACHINE = 'kvm'
65
LXC_MACHINE = 'lxc'
66
LXD_MACHINE = 'lxd'
67
1259.2.1 by Martin Packman
Add flag to run_deployer to use juju 2.0 bundle deployment
68
_DEFAULT_BUNDLE_TIMEOUT = 3600
69
1242.2.1 by Aaron Bentley
EnvJujuClient assumes JES enabled, since it assumes 2.0.
70
_jes_cmds = {KILL_CONTROLLER: {
1199 by Curtis Hovey
Revert lp:~sinzui/juju-ci-tools/kill-controller-command r1198 because it exposes that controllers are
71
    'create': 'create-environment',
1242.2.1 by Aaron Bentley
EnvJujuClient assumes JES enabled, since it assumes 2.0.
72
    'kill': KILL_CONTROLLER,
1306.1.2 by Curtis Hovey
Fix missing brace.
73
}}
1242.2.1 by Aaron Bentley
EnvJujuClient assumes JES enabled, since it assumes 2.0.
74
for super_cmd in [SYSTEM, CONTROLLER]:
1199 by Curtis Hovey
Revert lp:~sinzui/juju-ci-tools/kill-controller-command r1198 because it exposes that controllers are
75
    _jes_cmds[super_cmd] = {
76
        'create': '{} create-environment'.format(super_cmd),
77
        'kill': '{} kill'.format(super_cmd),
1306.1.1 by Curtis Hovey
Save spike to get leader and followers.
78
    }
1162.1.1 by Aaron Bentley
Abstract jes support, make enable_jes optional in assess_jes.
79
1091.4.1 by James Tunnicliffe
Merged upstream
80
log = logging.getLogger("jujupy")
81
163 by Curtis Hovey
Extracted the windows command incase it needs to be reused.
82
1504.1.1 by Aaron Bentley
Use specific class for config errors.
83
class IncompatibleConfigClass(Exception):
84
    """Raised when a client is initialised with the wrong config class."""
85
86
1564.2.3 by Aaron Bentley
Switch to soft deadline, handle as an exception.
87
class SoftDeadlineExceeded(Exception):
88
    """Raised when an overall client operation takes too long."""
1564.2.2 by Aaron Bentley
Add check_timeouts and ignore_deadline.
89
1647.1.1 by Aaron Bentley
Ignore soft deadline in safe_print_status.
90
    def __init__(self):
91
        super(SoftDeadlineExceeded, self).__init__(
92
            'Operation exceeded deadline.')
93
1564.2.2 by Aaron Bentley
Add check_timeouts and ignore_deadline.
94
1674.1.1 by Aaron Bentley
Add get_provider to jujupy.
95
class NoProvider(Exception):
96
    """Raised when an environment defines no provider."""
97
98
1727.2.17 by Aaron Bentley
Add bogus type testing.
99
class TypeNotAccepted(Exception):
100
    """Raised when the provided type was not accepted."""
101
102
1727.2.18 by Aaron Bentley
Check invalid name handling.
103
class NameNotAccepted(Exception):
104
    """Raised when the provided name was not accepted."""
105
106
1727.3.1 by Aaron Bentley
add_cloud_interative raises an exception if auth not accepted.
107
class AuthNotAccepted(Exception):
108
    """Raised when the provided auth was not accepted."""
109
110
1069.1.1 by Aaron Bentley
Replace timeout utility in _full_args.
111
def get_timeout_path():
112
    import timeout
113
    return os.path.abspath(timeout.__file__)
114
115
116
def get_timeout_prefix(duration, timeout_path=None):
117
    """Return extra arguments to run a command with a timeout."""
118
    if timeout_path is None:
119
        timeout_path = get_timeout_path()
120
    return (sys.executable, timeout_path, '%.2f' % duration, '--')
121
122
1535.1.4 by Curtis Hovey
Extracted get_teardown_timeout.
123
def get_teardown_timeout(client):
124
    """Return the timeout need byt the client to teardown resources."""
1674.1.3 by Aaron Bentley
Use provider rather than get_provider.
125
    if client.env.provider == 'azure':
1535.1.4 by Curtis Hovey
Extracted get_teardown_timeout.
126
        return 1800
1723 by Curtis Hovey
Give GCE more time to delete large deployments, Lp #1640587
127
    elif client.env.provider == 'gce':
128
        return 1200
1535.1.4 by Curtis Hovey
Extracted get_teardown_timeout.
129
    else:
130
        return 600
131
132
953.3.8 by Nate Finch
more review changes
133
def parse_new_state_server_from_error(error):
976.2.1 by Aaron Bentley
Fix parse_new_state_server_from_error
134
    err_str = str(error)
135
    output = getattr(error, 'output', None)
136
    if output is not None:
137
        err_str += output
138
    matches = re.findall(r'Attempting to connect to (.*):22', err_str)
953.3.8 by Nate Finch
more review changes
139
    if matches:
140
        return matches[-1]
141
    return None
142
143
2 by Aaron Bentley
Added initial deploy_stack.
144
class ErroredUnit(Exception):
145
301.1.3 by Aaron Bentley
Remove environment name from log messages and errors.
146
    def __init__(self, unit_name, state):
147
        msg = '%s is in state %s' % (unit_name, state)
19.1.14 by Aaron Bentley
More error handling fixes.
148
        Exception.__init__(self, msg)
771.2.1 by Aaron Bentley
Increase set of agent-state-infos that indicate failure.
149
        self.unit_name = unit_name
150
        self.state = state
2 by Aaron Bentley
Added initial deploy_stack.
151
152
1242.3.1 by Aaron Bentley
Update get_bootstrap_args.
153
class BootstrapMismatch(Exception):
154
155
    def __init__(self, arg_name, arg_val, env_name, env_val):
156
        super(BootstrapMismatch, self).__init__(
157
            '--{} {} does not match {}: {}'.format(
158
                arg_name, arg_val, env_name, env_val))
159
160
1258.2.5 by Curtis Hovey
Added upgrade_mongo to EnvJujuClient.
161
class UpgradeMongoNotSupported(Exception):
162
163
    def __init__(self):
164
        super(UpgradeMongoNotSupported, self).__init__(
165
            'This client does not support upgrade-mongo')
166
167
1044.1.9 by Aaron Bentley
Update and add tests.
168
class JESNotSupported(Exception):
169
170
    def __init__(self):
171
        super(JESNotSupported, self).__init__(
1183.1.9 by Curtis Hovey
Revert change that makes diff hard to read.
172
            'This client does not support JES')
1044.1.9 by Aaron Bentley
Update and add tests.
173
174
175
class JESByDefault(Exception):
176
177
    def __init__(self):
1044.1.13 by Aaron Bentley
Add more enable_jes tests.
178
        super(JESByDefault, self).__init__(
1183.1.9 by Curtis Hovey
Revert change that makes diff hard to read.
179
            'This client does not need to enable JES')
1183.1.2 by Curtis Hovey
Save point.
180
181
1662.1.1 by Andrew Beach
Cut out EnvJujuClient26. Will have to split it into smaller pieces for merging.
182
class VersionNotTestedError(Exception):
183
184
    def __init__(self, version):
185
        super(VersionNotTestedError, self).__init__(
186
            'Tests for juju {} are no longer supported.'.format(version))
187
188
1315.2.1 by Aaron Bentley
Use machine id instead of machine number.
189
Machine = namedtuple('Machine', ['machine_id', 'info'])
1306.1.7 by Curtis Hovey
Added get_controller_leader.
190
191
1246.1.2 by Curtis Hovey
Added coalesce_agent_status to handle machine agent-state and unit agent-status.
192
def coalesce_agent_status(agent_item):
193
    """Return the machine agent-state or the unit agent-status."""
194
    state = agent_item.get('agent-state')
195
    if state is None and agent_item.get('agent-status') is not None:
196
        state = agent_item.get('agent-status').get('current')
1293 by Curtis Hovey
Support juju-status.
197
    if state is None and agent_item.get('juju-status') is not None:
198
        state = agent_item.get('juju-status').get('current')
1246.1.2 by Curtis Hovey
Added coalesce_agent_status to handle machine agent-state and unit agent-status.
199
    if state is None:
200
        state = 'no-agent'
201
    return state
202
203
185.1.1 by Aaron Bentley
Fix cloud test handling of 'Unable to connect to environment.'
204
class CannotConnectEnv(subprocess.CalledProcessError):
205
206
    def __init__(self, e):
207
        super(CannotConnectEnv, self).__init__(e.returncode, e.cmd, e.output)
208
209
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
210
class StatusNotMet(Exception):
211
212
    _fmt = 'Expected status not reached in {env}.'
213
214
    def __init__(self, environment_name, status):
215
        self.env = environment_name
747.3.3 by Aaron Bentley
Implement retrying add-machine.
216
        self.status = status
217
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
218
    def __str__(self):
219
        return self._fmt.format(env=self.env)
220
221
222
class AgentsNotStarted(StatusNotMet):
223
224
    _fmt = 'Timed out waiting for agents to start in {env}.'
225
226
227
class VersionsNotUpdated(StatusNotMet):
228
229
    _fmt = 'Some versions did not update.'
230
231
232
class WorkloadsNotReady(StatusNotMet):
233
234
    _fmt = 'Workloads not ready in {env}.'
235
747.3.3 by Aaron Bentley
Implement retrying add-machine.
236
1738.2.1 by Andrew Beach
Added StatusNotMet exceptions for wait_for_{deploy_started,ha}.
237
class ApplicationsNotStarted(StatusNotMet):
238
239
    _fmt = 'Timed out waiting for applications to start in {env}.'
240
241
242
class VotingNotEnabled(StatusNotMet):
243
244
    _fmt = 'Timed out waiting for voting to be enabled in {env}.'
245
246
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
247
@contextmanager
248
def temp_yaml_file(yaml_dict):
1272.1.1 by Aaron Bentley
temp_yaml_file provides the name of a closed file, for Windows.
249
    temp_file = NamedTemporaryFile(suffix='.yaml', delete=False)
250
    try:
251
        with temp_file:
252
            yaml.safe_dump(yaml_dict, temp_file)
253
        yield temp_file.name
254
    finally:
255
        os.unlink(temp_file.name)
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
256
257
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
258
class SimpleEnvironment:
1606.1.1 by Aaron Bentley
Update docs.
259
    """Represents a model in a JUJU_HOME directory for juju 1."""
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
260
261
    def __init__(self, environment, config=None, juju_home=None,
262
                 controller=None):
1606.1.1 by Aaron Bentley
Update docs.
263
        """Constructor.
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
264
265
        :param environment: Name of the environment.
266
        :param config: Dictionary with configuration options, default is None.
267
        :param juju_home: Path to JUJU_HOME directory, default is None.
1606.1.1 by Aaron Bentley
Update docs.
268
        :param controller: Controller instance-- this model's controller.
269
            If not given or None a new instance is created."""
1477.2.12 by Leo Zhang
Fake merge of trunk
270
        self.user_name = None
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
271
        if controller is None:
272
            controller = Controller(environment)
273
        self.controller = controller
274
        self.environment = environment
275
        self.config = config
276
        self.juju_home = juju_home
277
        if self.config is not None:
1674.1.1 by Aaron Bentley
Add get_provider to jujupy.
278
            try:
1674.1.3 by Aaron Bentley
Use provider rather than get_provider.
279
                provider = self.provider
1674.1.1 by Aaron Bentley
Add get_provider to jujupy.
280
            except NoProvider:
281
                provider = None
282
            self.local = bool(provider == 'local')
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
283
            self.kvm = (
284
                self.local and bool(self.config.get('container') == 'kvm'))
1674.1.1 by Aaron Bentley
Add get_provider to jujupy.
285
            self.maas = bool(provider == 'maas')
286
            self.joyent = bool(provider == 'joyent')
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
287
        else:
288
            self.local = False
289
            self.kvm = False
290
            self.maas = False
291
            self.joyent = False
292
1699.1.1 by Aaron Bentley
Implement and use SimpleEnvironment and JujuData get_option.
293
    def get_option(self, key, default=None):
294
        return self.config.get(key, default)
295
1674.1.15 by Aaron Bentley
Add update_config method.
296
    def update_config(self, new_config):
297
        for key, value in new_config.items():
298
            if key == 'region':
299
                logging.warning(
300
                    'Using set_region to set region to "{}".'.format(value))
301
                self.set_region(value)
302
                continue
303
            if key == 'type':
304
                logging.warning('Setting type is not 2.x compatible.')
305
            self.config[key] = value
306
1674.1.3 by Aaron Bentley
Use provider rather than get_provider.
307
    @property
308
    def provider(self):
1674.1.1 by Aaron Bentley
Add get_provider to jujupy.
309
        """Return the provider type for this environment.
310
311
        See get_cloud to determine the specific cloud.
312
        """
313
        try:
314
            return self.config['type']
315
        except KeyError:
316
            raise NoProvider('No provider specified.')
317
1674.1.6 by Aaron Bentley
Move get_region to SimpleEnvironment.
318
    def get_region(self):
319
        provider = self.provider
320
        if provider == 'azure':
321
            if 'tenant-id' not in self.config:
322
                return self.config['location'].replace(' ', '').lower()
323
            return self.config['location']
324
        elif provider == 'joyent':
325
            matcher = re.compile('https://(.*).api.joyentcloud.com')
326
            return matcher.match(self.config['sdc-url']).group(1)
327
        elif provider == 'lxd':
1699.1.4 by Aaron Bentley
Allow arbitrary regions for lxd.
328
            return self.config.get('region', 'localhost')
1674.1.6 by Aaron Bentley
Move get_region to SimpleEnvironment.
329
        elif provider == 'manual':
330
            return self.config['bootstrap-host']
331
        elif provider in ('maas', 'manual'):
332
            return None
333
        else:
334
            return self.config['region']
335
1674.1.7 by Aaron Bentley
Implement SimpleEnvJujuClient.set_region.
336
    def set_region(self, region):
1674.1.8 by Aaron Bentley
set_region works with no provider for testing purposes.
337
        try:
338
            provider = self.provider
339
        except NoProvider:
340
            provider = None
1674.1.7 by Aaron Bentley
Implement SimpleEnvJujuClient.set_region.
341
        if provider == 'azure':
342
            self.config['location'] = region
343
        elif provider == 'joyent':
344
            self.config['sdc-url'] = (
345
                'https://{}.api.joyentcloud.com'.format(region))
346
        elif provider == 'manual':
347
            self.config['bootstrap-host'] = region
348
        elif provider == 'maas':
349
            if region is not None:
350
                raise ValueError('Only None allowed for maas.')
351
        else:
352
            self.config['region'] = region
353
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
354
    def clone(self, model_name=None):
355
        config = deepcopy(self.config)
356
        if model_name is None:
357
            model_name = self.environment
358
        else:
1477.3.1 by Andrew Wilkins
beebop
359
            config['name'] = unqualified_model_name(model_name)
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
360
        result = self.__class__(model_name, config, self.juju_home,
361
                                self.controller)
362
        result.local = self.local
363
        result.kvm = self.kvm
364
        result.maas = self.maas
365
        result.joyent = self.joyent
366
        return result
367
368
    def __eq__(self, other):
369
        if type(self) != type(other):
370
            return False
371
        if self.environment != other.environment:
372
            return False
373
        if self.config != other.config:
374
            return False
375
        if self.local != other.local:
376
            return False
377
        if self.maas != other.maas:
378
            return False
379
        return True
380
381
    def __ne__(self, other):
382
        return not self == other
383
384
    def set_model_name(self, model_name, set_controller=True):
385
        if set_controller:
386
            self.controller.name = model_name
387
        self.environment = model_name
1477.3.1 by Andrew Wilkins
beebop
388
        self.config['name'] = unqualified_model_name(model_name)
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
389
390
    @classmethod
391
    def from_config(cls, name):
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
392
        """Create an environment from the configuation file.
393
394
        :param name: Name of the environment to get the configuration from."""
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
395
        return cls._from_config(name)
396
397
    @classmethod
398
    def _from_config(cls, name):
399
        config, selected = get_selected_environment(name)
400
        if name is None:
401
            name = selected
402
        return cls(name, config)
403
404
    def needs_sudo(self):
405
        return self.local
406
407
    @contextmanager
408
    def make_jes_home(self, juju_home, dir_name, new_config):
1695.2.9 by Andrew Beach
Updated SimpleEnvironment.make_jes_home in order keep public-clouds.yaml in the home directory.
409
        """Make a JUJU_HOME/DATA directory to avoid conflicts.
410
411
        :param juju_home: Current JUJU_HOME/DATA directory, used as a
412
            base path for the new directory.
413
        :param dir_name: Name of sub-directory to make the home in.
1695.2.11 by Andrew Beach
Update in responce to merge request comments.
414
        :param new_config: Dictionary representing the contents of
1695.2.9 by Andrew Beach
Updated SimpleEnvironment.make_jes_home in order keep public-clouds.yaml in the home directory.
415
            the environments.yaml configuation file."""
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
416
        home_path = jes_home_path(juju_home, dir_name)
1703.1.6 by Andrew Beach
Renamed the function to skip_on_missing_file.
417
        with skip_on_missing_file():
1695.2.12 by Andrew Beach
Fixes for more comments.
418
            shutil.rmtree(home_path)
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
419
        os.makedirs(home_path)
420
        self.dump_yaml(home_path, new_config)
1695.2.11 by Andrew Beach
Update in responce to merge request comments.
421
        # For extention: Add all files carried over to the list.
422
        for file_name in ['public-clouds.yaml']:
423
            src_path = os.path.join(juju_home, file_name)
1703.1.6 by Andrew Beach
Renamed the function to skip_on_missing_file.
424
            with skip_on_missing_file():
1695.2.12 by Andrew Beach
Fixes for more comments.
425
                shutil.copy(src_path, home_path)
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
426
        yield home_path
427
1614.2.2 by Aaron Bentley
Implement SimpleEnvironment.get_cloud_credentials.
428
    def get_cloud_credentials(self):
429
        """Return the credentials for this model's cloud.
430
431
        This implementation returns config variables in addition to
432
        credentials.
433
        """
434
        return self.config
435
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
436
    def dump_yaml(self, path, config):
437
        dump_environments_yaml(path, config)
438
439
440
class JujuData(SimpleEnvironment):
1606.1.1 by Aaron Bentley
Update docs.
441
    """Represents a model in a JUJU_DATA directory for juju 2."""
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
442
443
    def __init__(self, environment, config=None, juju_home=None,
444
                 controller=None):
1606.1.1 by Aaron Bentley
Update docs.
445
        """Constructor.
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
446
447
        This extends SimpleEnvironment's constructor.
448
1606.1.1 by Aaron Bentley
Update docs.
449
        :param environment: Name of the environment.
450
        :param config: Dictionary with configuration options; default is None.
451
        :param juju_home: Path to JUJU_DATA directory. If None (the default),
452
            the home directory is autodetected.
453
        :param controller: Controller instance-- this model's controller.
454
            If not given or None, a new instance is created.
455
        """
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
456
        if juju_home is None:
457
            juju_home = get_juju_home()
458
        super(JujuData, self).__init__(environment, config, juju_home,
459
                                       controller)
460
        self.credentials = {}
461
        self.clouds = {}
462
463
    def clone(self, model_name=None):
464
        result = super(JujuData, self).clone(model_name)
465
        result.credentials = deepcopy(self.credentials)
466
        result.clouds = deepcopy(self.clouds)
467
        return result
468
469
    @classmethod
470
    def from_env(cls, env):
471
        juju_data = cls(env.environment, env.config, env.juju_home)
472
        juju_data.load_yaml()
473
        return juju_data
474
1674.1.15 by Aaron Bentley
Add update_config method.
475
    def update_config(self, new_config):
476
        if 'type' in new_config:
477
            raise ValueError('type cannot be set via update_config.')
478
        super(JujuData, self).update_config(new_config)
479
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
480
    def load_yaml(self):
481
        try:
482
            with open(os.path.join(self.juju_home, 'credentials.yaml')) as f:
483
                self.credentials = yaml.safe_load(f)
484
        except IOError as e:
485
            if e.errno != errno.ENOENT:
486
                raise RuntimeError(
487
                    'Failed to read credentials file: {}'.format(str(e)))
488
            self.credentials = {}
1711.6.10 by Aaron Bentley
Extract and use JujuData.read_clouds.
489
        self.clouds = self.read_clouds()
490
491
    def read_clouds(self):
492
        """Read and return clouds.yaml as a Python dict."""
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
493
        try:
494
            with open(os.path.join(self.juju_home, 'clouds.yaml')) as f:
1711.6.10 by Aaron Bentley
Extract and use JujuData.read_clouds.
495
                return yaml.safe_load(f)
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
496
        except IOError as e:
497
            if e.errno != errno.ENOENT:
498
                raise RuntimeError(
499
                    'Failed to read clouds file: {}'.format(str(e)))
500
            # Default to an empty clouds file.
1711.6.10 by Aaron Bentley
Extract and use JujuData.read_clouds.
501
            return {'clouds': {}}
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
502
503
    @classmethod
504
    def from_config(cls, name):
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
505
        """Create a model from the three configuration files."""
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
506
        juju_data = cls._from_config(name)
507
        juju_data.load_yaml()
508
        return juju_data
509
510
    def dump_yaml(self, path, config):
511
        """Dump the configuration files to the specified path.
512
513
        config is unused, but is accepted for compatibility with
514
        SimpleEnvironment and make_jes_home().
515
        """
516
        with open(os.path.join(path, 'credentials.yaml'), 'w') as f:
517
            yaml.safe_dump(self.credentials, f)
1711.6.12 by Aaron Bentley
Test assess_cloud.
518
        self.write_clouds(path, self.clouds)
519
520
    @staticmethod
521
    def write_clouds(path, clouds):
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
522
        with open(os.path.join(path, 'clouds.yaml'), 'w') as f:
1711.6.12 by Aaron Bentley
Test assess_cloud.
523
            yaml.safe_dump(clouds, f)
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
524
525
    def find_endpoint_cloud(self, cloud_type, endpoint):
1586.1.3 by Curtis Hovey
Revert find_endpoint_cloud and rmeove dangerous logging.
526
        for cloud, cloud_config in self.clouds['clouds'].items():
527
            if cloud_config['type'] != cloud_type:
528
                continue
529
            if cloud_config['endpoint'] == endpoint:
530
                return cloud
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
531
        raise LookupError('No such endpoint: {}'.format(endpoint))
532
533
    def get_cloud(self):
1674.1.3 by Aaron Bentley
Use provider rather than get_provider.
534
        provider = self.provider
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
535
        # Separate cloud recommended by: Juju Cloud / Credentials / BootStrap /
536
        # Model CLI specification
537
        if provider == 'ec2' and self.config['region'] == 'cn-north-1':
538
            return 'aws-china'
539
        if provider not in ('maas', 'openstack'):
540
            return {
541
                'ec2': 'aws',
542
                'gce': 'google',
543
            }.get(provider, provider)
544
        if provider == 'maas':
545
            endpoint = self.config['maas-server']
546
        elif provider == 'openstack':
547
            endpoint = self.config['auth-url']
548
        return self.find_endpoint_cloud(provider, endpoint)
549
1711.3.3 by Aaron Bentley
Let controller decide whether to supply cloud/region and credentials.
550
    def get_cloud_credentials_item(self):
1614.2.1 by Aaron Bentley
Extract credential retrieval from assess_autoload_credentials.
551
        cloud_name = self.get_cloud()
552
        cloud = self.credentials['credentials'][cloud_name]
1711.3.3 by Aaron Bentley
Let controller decide whether to supply cloud/region and credentials.
553
        (credentials_item,) = cloud.items()
554
        return credentials_item
555
556
    def get_cloud_credentials(self):
557
        """Return the credentials for this model's cloud."""
558
        return self.get_cloud_credentials_item()[1]
1614.2.1 by Aaron Bentley
Extract credential retrieval from assess_autoload_credentials.
559
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
560
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
561
class StatusError(Exception):
562
    """Generic error for Status."""
563
564
    recoverable = True
565
566
    # This has to be filled in after the classes are declared.
567
    ordering = []
568
569
    @classmethod
570
    def priority(cls):
571
        """Get the priority of the StatusError as an number.
572
573
        Lower number means higher priority. This can be used as a key
574
        function in sorting."""
575
        return cls.ordering.index(cls)
576
577
578
class MachineError(StatusError):
579
    """Error in machine-status."""
580
581
    recoverable = False
582
583
584
class UnitError(StatusError):
585
    """Error in a unit's status."""
586
587
588
class HookFailedError(UnitError):
589
    """A unit hook has failed."""
590
591
    def __init__(self, item_name, msg):
592
        match = re.search('^hook failed: "([^"]+)"$', msg)
593
        if match:
594
            msg = match.group(1)
595
        super(HookFailedError, self).__init__(item_name, msg)
596
597
598
class InstallError(HookFailedError):
599
    """The unit's install hook has failed."""
600
601
    recoverable = False
602
603
604
class AppError(StatusError):
605
    """Error in an application's status."""
606
607
608
class AgentError(StatusError):
609
    """Error in a juju agent."""
610
611
1726.1.3 by Andrew Beach
Agent{Long -> Unresolved}Error and added some tests for priority.
612
class AgentUnresolvedError(AgentError):
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
613
    """Agent error has not recovered in a reasonable time."""
614
1726.2.6 by Andrew Beach
Added documentation and moved the magic number of 5min out of the calculation.
615
    # This is the time limit set by IS for recovery from an agent error.
616
    a_reasonable_time = timedelta(minutes=5)
617
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
618
    recoverable = False
619
620
1726.1.3 by Andrew Beach
Agent{Long -> Unresolved}Error and added some tests for priority.
621
StatusError.ordering = [MachineError, InstallError, AgentUnresolvedError,
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
622
                        HookFailedError, UnitError, AppError, AgentError,
623
                        StatusError]
624
625
626
class StatusItem:
627
628
    APPLICATION = 'application-status'
629
    WORKLOAD = 'workload-status'
630
    MACHINE = 'machine-status'
631
    JUJU = 'juju-status'
632
633
    def __init__(self, status_name, item_name, item_value):
1726.2.6 by Andrew Beach
Added documentation and moved the magic number of 5min out of the calculation.
634
        """Create a new StatusItem from its fields.
635
636
        :param status_name: One of the status strings.
637
        :param item_name: The name of the machine/unit/application the status
638
            information is about.
639
        :param item_value: A dictionary of status values. If there is an entry
640
            with the status_name in the dictionary its contents are used."""
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
641
        self.status_name = status_name
642
        self.item_name = item_name
1726.2.5 by Andrew Beach
Fixes from comments and added to_exception tests.
643
        self.status = item_value.get(status_name, item_value)
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
644
645
    @property
646
    def message(self):
647
        return self.status.get('message')
648
649
    @property
650
    def since(self):
651
        return self.status.get('since')
652
653
    @property
654
    def current(self):
655
        return self.status.get('current')
656
657
    @property
658
    def version(self):
659
        return self.status.get('version')
660
1726.2.5 by Andrew Beach
Fixes from comments and added to_exception tests.
661
    @property
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
662
    def datetime_since(self):
1726.2.5 by Andrew Beach
Fixes from comments and added to_exception tests.
663
        if self.since is None:
664
            return None
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
665
        return datetime.strptime(self.since, '%d %b %Y %H:%M:%SZ')
666
667
    def to_exception(self):
668
        """Create an exception representing the error if one exists.
669
670
        :return: StatusError (or subtype) to represent an error or None
671
        to show that there is no error."""
672
        if self.current not in ['error', 'failed']:
673
            return None
674
675
        if self.APPLICATION == self.status_name:
676
            return AppError(self.item_name, self.message)
677
        elif self.WORKLOAD == self.status_name:
678
            if self.message is None:
679
                return UnitError(self.item_name, self.message)
1726.2.5 by Andrew Beach
Fixes from comments and added to_exception tests.
680
            elif re.match('hook failed: ".*install.*"', self.message):
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
681
                return InstallError(self.item_name, self.message)
682
            elif re.match('hook failed', self.message):
683
                return HookFailedError(self.item_name, self.message)
684
            else:
685
                return UnitError(self.item_name, self.message)
686
        elif self.MACHINE == self.status_name:
687
            return MachineError(self.item_name, self.message)
688
        elif self.JUJU == self.status_name:
1726.2.5 by Andrew Beach
Fixes from comments and added to_exception tests.
689
            time_since = datetime.utcnow() - self.datetime_since
1726.2.7 by Andrew Beach
Fixed to lint standard.
690
            if time_since > AgentUnresolvedError.a_reasonable_time:
1726.2.4 by Andrew Beach
Update Agent{Long -> Unresolved}Error name.
691
                return AgentUnresolvedError(self.item_name, self.message,
692
                                            time_since.total_seconds())
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
693
            else:
694
                return AgentError(self.item_name, self.message)
695
        else:
696
            raise ValueError('Unknown status:{}'.format(self.status_name),
697
                             (self.item_name, self.status_value))
698
1726.4.15 by Andrew Beach
It was a testing problem all along.
699
    def __repr__(self):
700
        return 'StatusItem({!r}, {!r}, {!r})'.format(
701
            self.status_name, self.item_name, self.status)
702
1726.1.1 by Andrew Beach
Grabbed changes from fix-wait-for-started.
703
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
704
class Status:
705
706
    def __init__(self, status, status_text):
707
        self.status = status
708
        self.status_text = status_text
709
710
    @classmethod
711
    def from_text(cls, text):
1437.1.1 by Aaron Bentley
Speed up tests by using JSON for status.
712
        try:
713
            # Parsing as JSON is much faster than parsing as YAML, so try
714
            # parsing as JSON first and fall back to YAML.
715
            status_yaml = json.loads(text)
716
        except ValueError:
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
717
            status_yaml = yaml.safe_load(text)
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
718
        return cls(status_yaml, text)
719
720
    def get_applications(self):
721
        return self.status.get('applications', {})
722
1726.3.4 by Andrew Beach
Reflecting some changes from down stream.
723
    def get_machines(self, default=None):
724
        return self.status.get('machines', default)
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
725
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
726
    def iter_machines(self, containers=False, machines=True):
727
        for machine_name, machine in sorted(self.status['machines'].items()):
728
            if machines:
729
                yield machine_name, machine
730
            if containers:
731
                for contained, unit in machine.get('containers', {}).items():
732
                    yield contained, unit
733
734
    def iter_new_machines(self, old_status):
735
        for machine, data in self.iter_machines():
736
            if machine in old_status.status['machines']:
737
                continue
738
            yield machine, data
739
1629.2.5 by Andrew Beach
Created a new fix for wait-for-started (in check_agents_started) that folds dying into the agent_states.
740
    def _iter_units_in_application(self, app_data):
741
        """Given application data, iterate through every unit in it."""
742
        for unit_name, unit in sorted(app_data.get('units', {}).items()):
743
            yield unit_name, unit
744
            subordinates = unit.get('subordinates', ())
745
            for sub_name in sorted(subordinates):
746
                yield sub_name, subordinates[sub_name]
747
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
748
    def iter_units(self):
1629.2.5 by Andrew Beach
Created a new fix for wait-for-started (in check_agents_started) that folds dying into the agent_states.
749
        """Iterate over every unit in every application."""
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
750
        for service_name, service in sorted(self.get_applications().items()):
1629.2.5 by Andrew Beach
Created a new fix for wait-for-started (in check_agents_started) that folds dying into the agent_states.
751
            for name, data in self._iter_units_in_application(service):
752
                yield name, data
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
753
754
    def agent_items(self):
755
        for machine_name, machine in self.iter_machines(containers=True):
756
            yield machine_name, machine
757
        for unit_name, unit in self.iter_units():
758
            yield unit_name, unit
759
1629.2.5 by Andrew Beach
Created a new fix for wait-for-started (in check_agents_started) that folds dying into the agent_states.
760
    def unit_agent_states(self, states=None):
761
        """Fill in a dictionary with the states of units.
762
763
        Units of a dying application are marked as dying.
764
765
        :param states: If not None, when it should be a defaultdict(list)),
766
        then states are added to this dictionary."""
767
        if states is None:
768
            states = defaultdict(list)
769
        for app_name, app_data in sorted(self.get_applications().items()):
770
            if app_data.get('life') == 'dying':
771
                for unit, data in self._iter_units_in_application(app_data):
772
                    states['dying'].append(unit)
773
            else:
774
                for unit, data in self._iter_units_in_application(app_data):
775
                    states[coalesce_agent_status(data)].append(unit)
776
        return states
777
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
778
    def agent_states(self):
779
        """Map agent states to the units and machines in those states."""
780
        states = defaultdict(list)
1629.2.5 by Andrew Beach
Created a new fix for wait-for-started (in check_agents_started) that folds dying into the agent_states.
781
        for item_name, item in self.iter_machines(containers=True):
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
782
            states[coalesce_agent_status(item)].append(item_name)
1629.2.5 by Andrew Beach
Created a new fix for wait-for-started (in check_agents_started) that folds dying into the agent_states.
783
        self.unit_agent_states(states)
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
784
        return states
785
786
    def check_agents_started(self, environment_name=None):
787
        """Check whether all agents are in the 'started' state.
788
789
        If not, return agent_states output.  If so, return None.
790
        If an error is encountered for an agent, raise ErroredUnit
791
        """
792
        bad_state_info = re.compile(
793
            '(.*error|^(cannot set up groups|cannot run instance)).*')
794
        for item_name, item in self.agent_items():
795
            state_info = item.get('agent-state-info', '')
796
            if bad_state_info.match(state_info):
797
                raise ErroredUnit(item_name, state_info)
798
        states = self.agent_states()
1629.2.5 by Andrew Beach
Created a new fix for wait-for-started (in check_agents_started) that folds dying into the agent_states.
799
        if set(states.keys()).issubset(AGENTS_READY):
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
800
            return None
801
        for state, entries in states.items():
802
            if 'error' in state:
1459.2.4 by Nicholas Skaggs
move logic as requested
803
                # sometimes the state may be hidden in juju status message
1459.2.5 by Nicholas Skaggs
minor tweaks per review
804
                juju_status = dict(
805
                    self.agent_items())[entries[0]].get('juju-status')
1459.2.4 by Nicholas Skaggs
move logic as requested
806
                if juju_status:
807
                    juju_status_msg = juju_status.get('message')
808
                    if juju_status_msg:
1459.2.5 by Nicholas Skaggs
minor tweaks per review
809
                        state = juju_status_msg
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
810
                raise ErroredUnit(entries[0], state)
811
        return states
812
813
    def get_service_count(self):
814
        return len(self.get_applications())
815
816
    def get_service_unit_count(self, service):
817
        return len(
818
            self.get_applications().get(service, {}).get('units', {}))
819
820
    def get_agent_versions(self):
821
        versions = defaultdict(set)
822
        for item_name, item in self.agent_items():
823
            if item.get('juju-status', None):
824
                version = item['juju-status'].get('version', 'unknown')
825
                versions[version].add(item_name)
826
            else:
827
                versions[item.get('agent-version', 'unknown')].add(item_name)
828
        return versions
829
830
    def get_instance_id(self, machine_id):
831
        return self.status['machines'][machine_id]['instance-id']
832
1631.4.3 by Andrew Beach
Added a check to assess_to to make sure the argument was used.
833
    def get_machine_dns_name(self, machine_id):
834
        return _dns_name_for_machine(self, machine_id)
835
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
836
    def get_unit(self, unit_name):
837
        """Return metadata about a unit."""
838
        for service in sorted(self.get_applications().values()):
839
            if unit_name in service.get('units', {}):
840
                return service['units'][unit_name]
841
        raise KeyError(unit_name)
842
843
    def service_subordinate_units(self, service_name):
844
        """Return subordinate metadata for a service_name."""
845
        services = self.get_applications()
846
        if service_name in services:
847
            for unit in sorted(services[service_name].get(
848
                    'units', {}).values()):
849
                for sub_name, sub in unit.get('subordinates', {}).items():
850
                    yield sub_name, sub
851
852
    def get_open_ports(self, unit_name):
853
        """List the open ports for the specified unit.
854
855
        If no ports are listed for the unit, the empty list is returned.
856
        """
857
        return self.get_unit(unit_name).get('open-ports', [])
858
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
859
    def iter_status(self):
860
        """Iterate through every status field in the larger status data."""
1726.4.1 by Andrew Beach
Tried actually using check_for_errors which highlighted some errors. I had to change a bunch of existing errors just to get meaningful errors out of them.
861
        for machine_name, machine_value in self.get_machines({}).items():
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
862
            yield StatusItem(StatusItem.MACHINE, machine_name, machine_value)
863
            yield StatusItem(StatusItem.JUJU, machine_name, machine_value)
864
        for app_name, app_value in self.get_applications().items():
865
            yield StatusItem(StatusItem.APPLICATION, app_name, app_value)
1743.1.1 by Andrew Beach
Allows for missing 'units' field in iter_status (both).
866
            for unit_name, unit_value in app_value.get('units', {}).items():
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
867
                yield StatusItem(StatusItem.WORKLOAD, unit_name, unit_value)
868
                yield StatusItem(StatusItem.JUJU, unit_name, unit_value)
869
870
    def iter_errors(self, ignore_recoverable=False):
871
        """Iterate through every error, repersented by exceptions."""
872
        for sub_status in self.iter_status():
873
            error = sub_status.to_exception()
874
            if error is not None:
875
                if not (ignore_recoverable and error.recoverable):
876
                    yield error
877
878
    def check_for_errors(self, ignore_recoverable=False):
879
        """Return a list of errors, in order of their priority."""
880
        return sorted(self.iter_errors(ignore_recoverable),
881
                      key=lambda item: item.priority())
882
883
    def raise_highest_error(self, ignore_recoverable=False):
884
        """Raise an exception reperenting the highest priority error."""
885
        errors = self.check_for_errors(ignore_recoverable)
886
        if errors:
887
            raise errors[0]
888
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
889
1692.1.1 by Andrew Beach
ServiceStatus->Status1X
890
class Status1X(Status):
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
891
892
    def get_applications(self):
893
        return self.status.get('services', {})
894
1726.3.7 by Andrew Beach
Added tests for the new code in Status1X.
895
    def condense_status(self, item_value):
896
        """Condense the scattered agent-* fields into a status dict."""
1726.4.13 by Andrew Beach
There where too many tests to add 'agent-status' and 'agent-version' too, so I had to make them optional.
897
        def shift_field(dest_dict, dest_name, src_dict, src_name):
898
            if src_name in src_dict:
899
                dest_dict[dest_name] = src_dict[src_name]
900
        condensed = {}
901
        shift_field(condensed, 'current', item_value, 'agent-state')
902
        shift_field(condensed, 'version', item_value, 'agent-version')
903
        shift_field(condensed, 'message', item_value, 'agent-state-info')
904
        return condensed
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
905
906
    def iter_status(self):
1726.3.7 by Andrew Beach
Added tests for the new code in Status1X.
907
        SERVICE = 'service-status'
908
        AGENT = 'agent-status'
1726.4.1 by Andrew Beach
Tried actually using check_for_errors which highlighted some errors. I had to change a bunch of existing errors just to get meaningful errors out of them.
909
        for machine_name, machine_value in self.get_machines({}).items():
1726.3.7 by Andrew Beach
Added tests for the new code in Status1X.
910
            yield StatusItem(StatusItem.JUJU, machine_name,
911
                             self.condense_status(machine_value))
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
912
        for app_name, app_value in self.get_applications().items():
1726.3.7 by Andrew Beach
Added tests for the new code in Status1X.
913
            if SERVICE in app_value:
914
                yield StatusItem(
915
                    StatusItem.APPLICATION, app_name,
916
                    {StatusItem.APPLICATION: app_value[SERVICE]})
1743.1.1 by Andrew Beach
Allows for missing 'units' field in iter_status (both).
917
            for unit_name, unit_value in app_value.get('units', {}).items():
1726.4.1 by Andrew Beach
Tried actually using check_for_errors which highlighted some errors. I had to change a bunch of existing errors just to get meaningful errors out of them.
918
                if StatusItem.WORKLOAD in unit_value:
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
919
                    yield StatusItem(StatusItem.WORKLOAD,
920
                                     unit_name, unit_value)
1726.3.7 by Andrew Beach
Added tests for the new code in Status1X.
921
                if AGENT in unit_value:
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
922
                    yield StatusItem(
923
                        StatusItem.JUJU, unit_name,
1726.3.7 by Andrew Beach
Added tests for the new code in Status1X.
924
                        {StatusItem.JUJU: unit_value[AGENT]})
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
925
                else:
1726.3.7 by Andrew Beach
Added tests for the new code in Status1X.
926
                    yield StatusItem(StatusItem.JUJU, unit_name,
927
                                     self.condense_status(unit_value))
1726.3.1 by Andrew Beach
Fake merge from status-status-item.
928
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
929
1699.1.3 by Aaron Bentley
Update describe_substrate to take env rather than raw config.
930
def describe_substrate(env):
931
    if env.provider == 'local':
932
        return {
933
            'kvm': 'KVM (local)',
934
            'lxc': 'LXC (local)'
935
        }[env.get_option('container', 'lxc')]
936
    elif env.provider == 'openstack':
937
        if env.get_option('auth-url') == (
938
                'https://keystone.canonistack.canonical.com:443/v2.0/'):
939
            return 'Canonistack'
940
        else:
941
            return 'Openstack'
942
    try:
943
        return {
944
            'ec2': 'AWS',
945
            'rackspace': 'Rackspace',
946
            'joyent': 'Joyent',
947
            'azure': 'Azure',
948
            'maas': 'MAAS',
949
        }[env.provider]
950
    except KeyError:
951
        return env.provider
952
953
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
954
class Juju2Backend:
1363.7.9 by Aaron Bentley
Clean up backend name and docs.
955
    """A Juju backend referring to a specific juju 2 binary.
956
957
    Uses -m to specify models, uses JUJU_DATA to specify home directory.
958
    """
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
959
1564.2.1 by Aaron Bentley
Add deadline support to full_args.
960
    def __init__(self, full_path, version, feature_flags, debug,
1564.2.3 by Aaron Bentley
Switch to soft deadline, handle as an exception.
961
                 soft_deadline=None):
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
962
        self._version = version
963
        self._full_path = full_path
1363.10.11 by Aaron Bentley
Handle feature flags in clone.
964
        self.feature_flags = feature_flags
1363.7.4 by Aaron Bentley
Move debug to backend.
965
        self.debug = debug
1363.7.3 by Aaron Bentley
Move _timeout_path to backend.
966
        self._timeout_path = get_timeout_path()
1363.8.1 by Aaron Bentley
Move juju_timings to backend.
967
        self.juju_timings = {}
1564.2.3 by Aaron Bentley
Switch to soft deadline, handle as an exception.
968
        self.soft_deadline = soft_deadline
969
        self._ignore_soft_deadline = False
1564.2.1 by Aaron Bentley
Add deadline support to full_args.
970
971
    def _now(self):
972
        return datetime.utcnow()
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
973
1564.2.2 by Aaron Bentley
Add check_timeouts and ignore_deadline.
974
    @contextmanager
975
    def _check_timeouts(self):
1564.2.3 by Aaron Bentley
Switch to soft deadline, handle as an exception.
976
        # If an exception occurred, we don't want to replace it with
977
        # SoftDeadlineExceeded.
978
        yield
979
        if self.soft_deadline is None or self._ignore_soft_deadline:
980
            return
981
        if self._now() > self.soft_deadline:
1647.1.1 by Aaron Bentley
Ignore soft deadline in safe_print_status.
982
            raise SoftDeadlineExceeded()
1564.2.2 by Aaron Bentley
Add check_timeouts and ignore_deadline.
983
984
    @contextmanager
1564.2.3 by Aaron Bentley
Switch to soft deadline, handle as an exception.
985
    def ignore_soft_deadline(self):
1564.2.2 by Aaron Bentley
Add check_timeouts and ignore_deadline.
986
        """Ignore the client deadline.  For cleanup code."""
1564.2.3 by Aaron Bentley
Switch to soft deadline, handle as an exception.
987
        old_val = self._ignore_soft_deadline
988
        self._ignore_soft_deadline = True
1564.2.2 by Aaron Bentley
Add check_timeouts and ignore_deadline.
989
        try:
990
            yield
991
        finally:
1564.2.3 by Aaron Bentley
Switch to soft deadline, handle as an exception.
992
            self._ignore_soft_deadline = old_val
1564.2.2 by Aaron Bentley
Add check_timeouts and ignore_deadline.
993
1363.10.11 by Aaron Bentley
Handle feature flags in clone.
994
    def clone(self, full_path, version, debug, feature_flags):
1363.9.6 by Aaron Bentley
Tweak cloning.
995
        if version is None:
996
            version = self.version
997
        if full_path is None:
998
            full_path = self.full_path
999
        if debug is None:
1000
            debug = self.debug
1575.2.9 by Aaron Bentley
Retain soft_deadline when cloning.
1001
        result = self.__class__(full_path, version, feature_flags, debug,
1002
                                self.soft_deadline)
1363.9.6 by Aaron Bentley
Tweak cloning.
1003
        return result
1004
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
1005
    @property
1006
    def version(self):
1007
        return self._version
1008
1009
    @property
1010
    def full_path(self):
1011
        return self._full_path
1012
1433.1.1 by Aaron Bentley
Use supplied juju binary name.
1013
    @property
1014
    def juju_name(self):
1015
        return os.path.basename(self._full_path)
1016
1363.8.1 by Aaron Bentley
Move juju_timings to backend.
1017
    def _get_attr_tuple(self):
1018
        return (self._version, self._full_path, self.feature_flags,
1019
                self.debug, self.juju_timings)
1020
1363.6.5 by Aaron Bentley
Store feature flags on backend.
1021
    def __eq__(self, other):
1022
        if type(self) != type(other):
1023
            return False
1363.8.1 by Aaron Bentley
Move juju_timings to backend.
1024
        return self._get_attr_tuple() == other._get_attr_tuple()
1363.6.5 by Aaron Bentley
Store feature flags on backend.
1025
1363.7.1 by Aaron Bentley
Implement _shell_environ on backend.
1026
    def shell_environ(self, used_feature_flags, juju_home):
1027
        """Generate a suitable shell environment.
1028
1029
        Juju's directory must be in the PATH to support plugins.
1030
        """
1031
        env = dict(os.environ)
1032
        if self.full_path is not None:
1033
            env['PATH'] = '{}{}{}'.format(os.path.dirname(self.full_path),
1034
                                          os.pathsep, env['PATH'])
1035
        flags = self.feature_flags.intersection(used_feature_flags)
1717.1.1 by Andrew Beach
Added code and tests for keeping existing values of JUJU_DEV_FEATURE_FLAGS in a shell_environ.
1036
        feature_flag_string = env.get(JUJU_DEV_FEATURE_FLAGS, '')
1037
        if feature_flag_string != '':
1038
            flags.update(feature_flag_string.split(','))
1363.7.1 by Aaron Bentley
Implement _shell_environ on backend.
1039
        if flags:
1040
            env[JUJU_DEV_FEATURE_FLAGS] = ','.join(sorted(flags))
1041
        env['JUJU_DATA'] = juju_home
1042
        return env
1043
1363.7.4 by Aaron Bentley
Move debug to backend.
1044
    def full_args(self, command, args, model, timeout):
1363.7.2 by Aaron Bentley
Move shell_environ to Backend.
1045
        if model is not None:
1046
            e_arg = ('-m', model)
1047
        else:
1048
            e_arg = ()
1049
        if timeout is None:
1050
            prefix = ()
1051
        else:
1363.7.3 by Aaron Bentley
Move _timeout_path to backend.
1052
            prefix = get_timeout_prefix(timeout, self._timeout_path)
1363.7.4 by Aaron Bentley
Move debug to backend.
1053
        logging = '--debug' if self.debug else '--show-log'
1363.7.2 by Aaron Bentley
Move shell_environ to Backend.
1054
1055
        # If args is a string, make it a tuple. This makes writing commands
1056
        # with one argument a bit nicer.
1057
        if isinstance(args, basestring):
1058
            args = (args,)
1059
        # we split the command here so that the caller can control where the -m
1060
        # model flag goes.  Everything in the command string is put before the
1061
        # -m flag.
1062
        command = command.split()
1433.1.1 by Aaron Bentley
Use supplied juju binary name.
1063
        return (prefix + (self.juju_name, logging,) + tuple(command) + e_arg +
1064
                args)
1363.7.2 by Aaron Bentley
Move shell_environ to Backend.
1065
1363.8.2 by Aaron Bentley
Move juju implementation to backend.
1066
    def juju(self, command, args, used_feature_flags,
1067
             juju_home, model=None, check=True, timeout=None, extra_env=None):
1068
        """Run a command under juju for the current environment."""
1069
        args = self.full_args(command, args, model, timeout)
1070
        log.info(' '.join(args))
1071
        env = self.shell_environ(used_feature_flags, juju_home)
1072
        if extra_env is not None:
1073
            env.update(extra_env)
1074
        if check:
1075
            call_func = subprocess.check_call
1076
        else:
1077
            call_func = subprocess.call
1078
        start_time = time.time()
1079
        # Mutate os.environ instead of supplying env parameter so Windows can
1080
        # search env['PATH']
1081
        with scoped_environ(env):
1564.2.4 by Aaron Bentley
Implement soft deadline on basic juju operations.
1082
            with self._check_timeouts():
1083
                rval = call_func(args)
1363.8.2 by Aaron Bentley
Move juju implementation to backend.
1084
        self.juju_timings.setdefault(args, []).append(
1085
            (time.time() - start_time))
1086
        return rval
1087
1410.2.4 by Christopher Lee
Fix EnvJujuClient.expect to ensure it uses the provided path within envvar.
1088
    def expect(self, command, args, used_feature_flags, juju_home, model=None,
1089
               timeout=None, extra_env=None):
1090
        args = self.full_args(command, args, model, timeout)
1091
        log.info(' '.join(args))
1092
        env = self.shell_environ(used_feature_flags, juju_home)
1093
        if extra_env is not None:
1094
            env.update(extra_env)
1095
        # pexpect.spawn expects a string. This is better than trying to extract
1096
        # command + args from the returned tuple (as there could be an intial
1097
        # timing command tacked on).
1098
        command_string = ' '.join(quote(a) for a in args)
1099
        with scoped_environ(env):
1100
            return pexpect.spawn(command_string)
1101
1363.8.8 by Aaron Bentley
Move juju_async to backend.
1102
    @contextmanager
1103
    def juju_async(self, command, args, used_feature_flags,
1104
                   juju_home, model=None, timeout=None):
1105
        full_args = self.full_args(command, args, model, timeout)
1106
        log.info(' '.join(args))
1107
        env = self.shell_environ(used_feature_flags, juju_home)
1108
        # Mutate os.environ instead of supplying env parameter so Windows can
1109
        # search env['PATH']
1110
        with scoped_environ(env):
1564.2.4 by Aaron Bentley
Implement soft deadline on basic juju operations.
1111
            with self._check_timeouts():
1112
                proc = subprocess.Popen(full_args)
1363.8.8 by Aaron Bentley
Move juju_async to backend.
1113
        yield proc
1114
        retcode = proc.wait()
1115
        if retcode != 0:
1116
            raise subprocess.CalledProcessError(retcode, full_args)
1117
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
1118
    def get_juju_output(self, command, args, used_feature_flags, juju_home,
1119
                        model=None, timeout=None, user_name=None,
1120
                        merge_stderr=False):
1363.8.6 by Aaron Bentley
Implement get_juju_output on backend.
1121
        args = self.full_args(command, args, model, timeout)
1122
        env = self.shell_environ(used_feature_flags, juju_home)
1123
        log.debug(args)
1124
        # Mutate os.environ instead of supplying env parameter so
1125
        # Windows can search env['PATH']
1126
        with scoped_environ(env):
1127
            proc = subprocess.Popen(
1128
                args, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
1129
                stderr=subprocess.STDOUT if merge_stderr else subprocess.PIPE)
1564.2.4 by Aaron Bentley
Implement soft deadline on basic juju operations.
1130
            with self._check_timeouts():
1131
                sub_output, sub_error = proc.communicate()
1363.8.6 by Aaron Bentley
Implement get_juju_output on backend.
1132
            log.debug(sub_output)
1133
            if proc.returncode != 0:
1134
                log.debug(sub_error)
1135
                e = subprocess.CalledProcessError(
1136
                    proc.returncode, args, sub_output)
1137
                e.stderr = sub_error
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
1138
                if sub_error and (
1363.8.6 by Aaron Bentley
Implement get_juju_output on backend.
1139
                    'Unable to connect to environment' in sub_error or
1140
                        'MissingOrIncorrectVersionHeader' in sub_error or
1141
                        '307: Temporary Redirect' in sub_error):
1142
                    raise CannotConnectEnv(e)
1143
                raise e
1144
        return sub_output
1145
1363.9.2 by Aaron Bentley
Move pause to backend.
1146
    def pause(self, seconds):
1147
        pause(seconds)
1148
1363.7.1 by Aaron Bentley
Implement _shell_environ on backend.
1149
1662.1.6 by Andrew Beach
Removed Juju2A2Backend.
1150
class Juju1XBackend(Juju2Backend):
1151
    """Backend for Juju 1.x versions.
1363.7.9 by Aaron Bentley
Clean up backend name and docs.
1152
1153
    Uses -e to specify models ("environments", uses JUJU_HOME to specify home
1154
    directory.
1155
    """
1363.7.1 by Aaron Bentley
Implement _shell_environ on backend.
1156
1157
    def shell_environ(self, used_feature_flags, juju_home):
1158
        """Generate a suitable shell environment.
1159
1160
        For 2.0-alpha1 and earlier set only JUJU_HOME and not JUJU_DATA.
1161
        """
1363.7.9 by Aaron Bentley
Clean up backend name and docs.
1162
        env = super(Juju1XBackend, self).shell_environ(used_feature_flags,
1163
                                                       juju_home)
1363.7.1 by Aaron Bentley
Implement _shell_environ on backend.
1164
        env['JUJU_HOME'] = juju_home
1165
        del env['JUJU_DATA']
1166
        return env
1167
1363.7.4 by Aaron Bentley
Move debug to backend.
1168
    def full_args(self, command, args, model, timeout):
1363.7.2 by Aaron Bentley
Move shell_environ to Backend.
1169
        if model is None:
1170
            e_arg = ()
1171
        else:
1172
            # In 1.x terminology, "model" is "environment".
1173
            e_arg = ('-e', model)
1174
        if timeout is None:
1175
            prefix = ()
1176
        else:
1363.7.3 by Aaron Bentley
Move _timeout_path to backend.
1177
            prefix = get_timeout_prefix(timeout, self._timeout_path)
1363.7.4 by Aaron Bentley
Move debug to backend.
1178
        logging = '--debug' if self.debug else '--show-log'
1363.7.2 by Aaron Bentley
Move shell_environ to Backend.
1179
1180
        # If args is a string, make it a tuple. This makes writing commands
1181
        # with one argument a bit nicer.
1182
        if isinstance(args, basestring):
1183
            args = (args,)
1184
        # we split the command here so that the caller can control where the -e
1185
        # <env> flag goes.  Everything in the command string is put before the
1186
        # -e flag.
1187
        command = command.split()
1433.1.1 by Aaron Bentley
Use supplied juju binary name.
1188
        return (prefix + (self.juju_name, logging,) + tuple(command) + e_arg +
1189
                args)
1363.7.2 by Aaron Bentley
Move shell_environ to Backend.
1190
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
1191
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1192
def get_client_class(version):
1193
    if version.startswith('1.16'):
1662.1.1 by Andrew Beach
Cut out EnvJujuClient26. Will have to split it into smaller pieces for merging.
1194
        raise VersionNotTestedError(version)
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1195
    elif re.match('^1\.22[.-]', version):
1196
        client_class = EnvJujuClient22
1197
    elif re.match('^1\.24[.-]', version):
1198
        client_class = EnvJujuClient24
1199
    elif re.match('^1\.25[.-]', version):
1200
        client_class = EnvJujuClient25
1201
    elif re.match('^1\.26[.-]', version):
1662.1.1 by Andrew Beach
Cut out EnvJujuClient26. Will have to split it into smaller pieces for merging.
1202
        raise VersionNotTestedError(version)
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1203
    elif re.match('^1\.', version):
1204
        client_class = EnvJujuClient1X
1662.1.16 by Andrew Beach
Used JESNotSupported to remove unsupported functionality from EnvJujuClient1X.
1205
    elif re.match('^2\.0-(alpha|beta)', version):
1662.1.4 by Andrew Beach
Deleted EnvJujuClient2A1 and removed support for testing with it.
1206
        raise VersionNotTestedError(version)
1646.1.1 by Christopher Lee
Update order of bootstrap args for 2.0+ (RC clients included, w/ test). Updated tests.
1207
    elif re.match('^2\.0-rc[1-3]', version):
1208
        client_class = EnvJujuClientRC
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1209
    else:
1210
        client_class = EnvJujuClient
1211
    return client_class
1212
1213
1575.2.1 by Aaron Bentley
Shove dealine from arg down to client_from_config.
1214
def client_from_config(config, juju_path, debug=False, soft_deadline=None):
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
1215
    """Create a client from an environment's configuration.
1216
1217
    :param config: Name of the environment to use the config from.
1218
    :param juju_path: Path to juju binary the client should wrap.
1575.3.7 by Aaron Bentley
Update docs.
1219
    :param debug=False: The debug flag for the client, False by default.
1220
    :param soft_deadline: A datetime representing the deadline by which
1221
        normal operations should complete.  If None, no deadline is
1222
        enforced.
1223
    """
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1224
    version = EnvJujuClient.get_version(juju_path)
1225
    client_class = get_client_class(version)
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
1226
    env = client_class.config_class.from_config(config)
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1227
    if juju_path is None:
1228
        full_path = EnvJujuClient.get_full_path()
1229
    else:
1230
        full_path = os.path.abspath(juju_path)
1575.2.1 by Aaron Bentley
Shove dealine from arg down to client_from_config.
1231
    return client_class(env, version, full_path, debug=debug,
1232
                        soft_deadline=soft_deadline)
1465.5.1 by Aaron Bentley
Implement client_from_config in terms of EnvJujuClient.by_version.
1233
1234
657.1.1 by Aaron Bentley
Extract EnvJujuClient from JujuClientDevel.
1235
class EnvJujuClient:
1600.1.2 by Andrew Beach
Various spelling mistakes.
1236
    """Wraps calls to a juju instance, associated with a single model.
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
1237
1606.1.1 by Aaron Bentley
Update docs.
1238
    Note: A model is often called an enviroment (Juju 1 legacy).
1239
1240
    This class represents the latest Juju version.  Subclasses are used to
1241
    support older versions (see get_client_class).
1242
    """
657.1.1 by Aaron Bentley
Extract EnvJujuClient from JujuClientDevel.
1243
1251.2.4 by Aaron Bentley
Rename bootstrap_supports => bootstrap_replaces
1244
    # The environments.yaml options that are replaced by bootstrap options.
1245
    #
1242.3.8 by Aaron Bentley
Override default-series even when --bootstrap-series is supplied.
1246
    # As described in bug #1538735, default-series and --bootstrap-series must
1251.2.5 by Aaron Bentley
Tweak verbiage.
1247
    # match.  'default-series' should be here, but is omitted so that
1248
    # default-series is always forced to match --bootstrap-series.
1251.2.4 by Aaron Bentley
Rename bootstrap_supports => bootstrap_replaces
1249
    bootstrap_replaces = frozenset(['agent-version'])
1242.3.7 by Aaron Bentley
BootstrapManager support for bootstrap option.
1250
1263.1.1 by Martin Packman
Refactor how juju feature flags are handled with new enable_feature method
1251
    # What feature flags have existed that CI used.
1662.1.3 by Andrew Beach
Removed the 'address-allocation' and 'cloudsigma' feature flags.
1252
    known_feature_flags = frozenset(['actions', 'jes', 'migration'])
1263.1.1 by Martin Packman
Refactor how juju feature flags are handled with new enable_feature method
1253
1254
    # What feature flags are used by this version of the juju client.
1662.1.3 by Andrew Beach
Removed the 'address-allocation' and 'cloudsigma' feature flags.
1255
    used_feature_flags = frozenset(['migration'])
1263.1.1 by Martin Packman
Refactor how juju feature flags are handled with new enable_feature method
1256
1402.3.8 by Leo Zhang
More updates after review
1257
    destroy_model_command = 'destroy-model'
1258
1341.2.1 by Aaron Bentley
Move container support knowledge to jujupy.
1259
    supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE,
1260
                                           LXD_MACHINE])
1261
1363.7.10 by Aaron Bentley
Use default_backend instead of overriding __init__.
1262
    default_backend = Juju2Backend
1263
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
1264
    config_class = JujuData
1265
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
1266
    status_class = Status
1267
1540.3.2 by Christopher Lee
Change tools_ to agent_
1268
    agent_metadata_url = 'agent-metadata-url'
1540.3.1 by Christopher Lee
Update upgrade methods to work with juju 2.0. Upgrades controller first then any other hosted models.
1269
1541.2.8 by Curtis Hovey
Remove Use set-model-config instrad of deploy.
1270
    model_permissions = frozenset(['read', 'write', 'admin'])
1541.2.6 by Curtis Hovey
Move new behaviours into EnvJujuClient.
1271
1541.2.8 by Curtis Hovey
Remove Use set-model-config instrad of deploy.
1272
    controller_permissions = frozenset(['login', 'addmodel', 'superuser'])
1541.2.6 by Curtis Hovey
Move new behaviours into EnvJujuClient.
1273
1657.1.1 by Martin Packman
Work in progress network safe endpoint binding test
1274
    reserved_spaces = frozenset([
1275
        'endpoint-bindings-data', 'endpoint-bindings-public'])
1276
1725.1.1 by Curtis Hovey
Rename disable_command_* to command_set_*.
1277
    command_set_destroy_model = 'destroy-model'
1278
1279
    command_set_remove_object = 'remove-object'
1280
1281
    command_set_all = 'all'
1723.1.1 by Curtis Hovey
Updated assess_block and jujupy to supprt juju 1x blocks.
1282
1341.2.5 by Aaron Bentley
Allow {container} expansion in bundle names.
1283
    @classmethod
1284
    def preferred_container(cls):
1285
        for container_type in [LXD_MACHINE, LXC_MACHINE]:
1286
            if container_type in cls.supported_container_types:
1287
                return container_type
1288
1221.5.15 by Aaron Bentley
Use 'show-status' instead of 'status'.
1289
    _show_status = 'show-status'
1290
657.1.7 by Aaron Bentley
Clean-up.
1291
    @classmethod
650.1.9 by Aaron Bentley
Merged trunk into compatibility-test.
1292
    def get_version(cls, juju_path=None):
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
1293
        """Get the version data from a juju binary.
1294
1606.1.1 by Aaron Bentley
Update docs.
1295
        :param juju_path: Path to binary. If not given or None, 'juju' is used.
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
1296
        """
650.1.1 by Aaron Bentley
Add juju_path to from_config
1297
        if juju_path is None:
1298
            juju_path = 'juju'
1299
        return subprocess.check_output((juju_path, '--version')).strip()
657.1.7 by Aaron Bentley
Clean-up.
1300
1564.2.6 by Aaron Bentley
Ignore soft deadlines for tearing down runtime_context.
1301
    def check_timeouts(self):
1302
        return self._backend._check_timeouts()
1303
1304
    def ignore_soft_deadline(self):
1305
        return self._backend.ignore_soft_deadline()
1306
1263.1.1 by Martin Packman
Refactor how juju feature flags are handled with new enable_feature method
1307
    def enable_feature(self, flag):
1308
        """Enable juju feature by setting the given flag.
1309
1310
        New versions of juju with the feature enabled by default will silently
1311
        allow this call, but will not export the environment variable.
1312
        """
1313
        if flag not in self.known_feature_flags:
1314
            raise ValueError('Unknown feature flag: %r' % (flag,))
1315
        self.feature_flags.add(flag)
1316
1242.2.1 by Aaron Bentley
EnvJujuClient assumes JES enabled, since it assumes 2.0.
1317
    def get_jes_command(self):
1318
        """For Juju 2.0, this is always kill-controller."""
1319
        return KILL_CONTROLLER
1320
1145.2.11 by Curtis Hovey
Let the client set permanent based on client.is_jes_enabled.
1321
    def is_jes_enabled(self):
1145.2.2 by Curtis Hovey
Extracted get_jes_command from is_jes_enabled.
1322
        """Does the state-server support multiple environments."""
1145.2.6 by Curtis Hovey
Raise JESNotSupported from get_jes_command when jes is not supported.
1323
        try:
1145.2.7 by Curtis Hovey
Remove get_jes_command cache. Restored test that simulated the change in help output caused by the jes feature flag.
1324
            self.get_jes_command()
1145.2.6 by Curtis Hovey
Raise JESNotSupported from get_jes_command when jes is not supported.
1325
            return True
1326
        except JESNotSupported:
1327
            return False
1145.2.2 by Curtis Hovey
Extracted get_jes_command from is_jes_enabled.
1328
1162.2.21 by Aaron Bentley
Implement and use tear_down(try_jes=True)
1329
    def enable_jes(self):
1330
        """Enable JES if JES is optional.
1331
1332
        Specifically implemented by the clients that optionally support JES.
1333
        This version raises either JESByDefault or JESNotSupported.
1334
1335
        :raises: JESByDefault when JES is always enabled; Juju has the
1336
            'destroy-controller' command.
1337
        :raises: JESNotSupported when JES is not supported; Juju does not have
1338
            the 'system kill' command when the JES feature flag is set.
1339
        """
1340
        if self.is_jes_enabled():
1341
            raise JESByDefault()
1342
        else:
1343
            raise JESNotSupported()
1344
657.1.7 by Aaron Bentley
Clean-up.
1345
    @classmethod
1346
    def get_full_path(cls):
161 by Curtis Hovey
Windows and py3 compatability.
1347
        if sys.platform == 'win32':
163 by Curtis Hovey
Extracted the windows command incase it needs to be reused.
1348
            return WIN_JUJU_CMD
107.1.2 by Aaron Bentley
Use full path when running under sudo.
1349
        return subprocess.check_output(('which', 'juju')).rstrip('\n')
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
1350
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1351
    def clone_path_cls(self, juju_path):
1352
        """Clone using the supplied path to determine the class."""
1353
        version = self.get_version(juju_path)
1354
        cls = get_client_class(version)
1465.1.1 by Aaron Bentley
Implement client_from_config.
1355
        if juju_path is None:
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1356
            full_path = self.get_full_path()
1465.1.1 by Aaron Bentley
Implement client_from_config.
1357
        else:
1358
            full_path = os.path.abspath(juju_path)
1465.5.2 by Aaron Bentley
Implement client_from_config, delete by_version.
1359
        return self.clone(version=version, full_path=full_path, cls=cls)
1465.1.1 by Aaron Bentley
Implement client_from_config.
1360
1221.1.9 by Aaron Bentley
Further extend clone().
1361
    def clone(self, env=None, version=None, full_path=None, debug=None,
1362
              cls=None):
1363
        """Create a clone of this EnvJujuClient.
1364
1365
        By default, the class, environment, version, full_path, and debug
1366
        settings will match the original, but each can be overridden.
1367
        """
1221.1.8 by Aaron Bentley
Implement EnvJujuClient.clone.
1368
        if env is None:
1369
            env = self.env
1221.1.9 by Aaron Bentley
Further extend clone().
1370
        if cls is None:
1371
            cls = self.__class__
1363.10.11 by Aaron Bentley
Handle feature flags in clone.
1372
        feature_flags = self.feature_flags.intersection(cls.used_feature_flags)
1373
        backend = self._backend.clone(full_path, version, debug, feature_flags)
1363.9.6 by Aaron Bentley
Tweak cloning.
1374
        other = cls.from_backend(backend, env)
1657.1.1 by Martin Packman
Work in progress network safe endpoint binding test
1375
        other.excluded_spaces = set(self.excluded_spaces)
1263.1.1 by Martin Packman
Refactor how juju feature flags are handled with new enable_feature method
1376
        return other
1221.1.8 by Aaron Bentley
Implement EnvJujuClient.clone.
1377
1363.9.6 by Aaron Bentley
Tweak cloning.
1378
    @classmethod
1379
    def from_backend(cls, backend, env):
1363.9.7 by Aaron Bentley
FakeJujuClient.clone uses real from_backend.
1380
        return cls(env=env, version=backend.version,
1381
                   full_path=backend.full_path,
1363.9.6 by Aaron Bentley
Tweak cloning.
1382
                   debug=backend.debug, _backend=backend)
1383
1240.2.1 by Aaron Bentley
Support models/cache.yaml
1384
    def get_cache_path(self):
1385
        return get_cache_path(self.env.juju_home, models=True)
1386
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1387
    def _cmd_model(self, include_e, controller):
1388
        if controller:
1468.1.1 by Christopher Lee
Fix cmd_model case where admin=True
1389
            return '{controller}:{model}'.format(
1390
                controller=self.env.controller.name,
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1391
                model=self.get_controller_model_name())
1363.8.8 by Aaron Bentley
Move juju_async to backend.
1392
        elif self.env is None or not include_e:
1393
            return None
1394
        else:
1427.1.1 by Christopher Lee
Update commands to use <controller>:<model> format. Fix arg to -c to be controller name.
1395
            return '{controller}:{model}'.format(
1396
                controller=self.env.controller.name,
1397
                model=self.model_name)
1363.8.8 by Aaron Bentley
Move juju_async to backend.
1398
1306.1.24 by Curtis Hovey
Added admin=False to _full_call_args and get_status().
1399
    def _full_args(self, command, sudo, args,
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1400
                   timeout=None, include_e=True, controller=False):
1401
        model = self._cmd_model(include_e, controller)
1363.7.2 by Aaron Bentley
Move shell_environ to Backend.
1402
        # sudo is not needed for devel releases.
1363.7.4 by Aaron Bentley
Move debug to backend.
1403
        return self._backend.full_args(command, args, model, timeout)
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
1404
1260.2.6 by Aaron Bentley
Introduce JujuData, remove workarounds for cloud-credentials.
1405
    @staticmethod
1406
    def _get_env(env):
1260.2.11 by Aaron Bentley
Get jujupy tests passing, remove region for manual.
1407
        if not isinstance(env, JujuData) and isinstance(env,
1408
                                                        SimpleEnvironment):
1260.2.23 by Aaron Bentley
Test SimpleEnvironment/JujuData on init.
1409
            # FIXME: JujuData should be used from the start.
1260.2.6 by Aaron Bentley
Introduce JujuData, remove workarounds for cloud-credentials.
1410
            env = JujuData.from_env(env)
1411
        return env
1412
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
1413
    def __init__(self, env, version, full_path, juju_home=None, debug=False,
1575.2.1 by Aaron Bentley
Shove dealine from arg down to client_from_config.
1414
                 soft_deadline=None, _backend=None):
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
1415
        """Create a new juju client.
1416
1606.1.1 by Aaron Bentley
Update docs.
1417
        Required Arguments
1418
        :param env: Object representing a model in a data directory.
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
1419
        :param version: Version of juju the client wraps.
1420
        :param full_path: Full path to juju binary.
1421
1606.1.1 by Aaron Bentley
Update docs.
1422
        Optional Arguments
1423
        :param juju_home: default value for env.juju_home.  Will be
1424
            autodetected if None (the default).
1425
        :param debug: Flag to activate debugging output; False by default.
1575.3.7 by Aaron Bentley
Update docs.
1426
        :param soft_deadline: A datetime representing the deadline by which
1427
            normal operations should complete.  If None, no deadline is
1428
            enforced.
1606.1.1 by Aaron Bentley
Update docs.
1429
        :param _backend: The backend to use for interacting with the client.
1430
            If None (the default), self.default_backend will be used.
1431
        """
1260.2.6 by Aaron Bentley
Introduce JujuData, remove workarounds for cloud-credentials.
1432
        self.env = self._get_env(env)
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
1433
        if _backend is None:
1575.2.1 by Aaron Bentley
Shove dealine from arg down to client_from_config.
1434
            _backend = self.default_backend(full_path, version, set(), debug,
1435
                                            soft_deadline)
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
1436
        self._backend = _backend
1437
        if version != _backend.version:
1438
            raise ValueError('Version mismatch: {} {}'.format(
1439
                version, _backend.version))
1440
        if full_path != _backend.full_path:
1441
            raise ValueError('Path mismatch: {} {}'.format(
1442
                full_path, _backend.full_path))
1363.7.4 by Aaron Bentley
Move debug to backend.
1443
        if debug is not _backend.debug:
1444
            raise ValueError('debug mismatch: {} {}'.format(
1445
                debug, _backend.debug))
1193.2.1 by Aaron Bentley
Make EnvJujuClient.juju_home a reference to EnvJUjuClient.env.juju_home.
1446
        if env is not None:
1447
            if juju_home is None:
1448
                if env.juju_home is None:
1449
                    env.juju_home = get_juju_home()
1450
            else:
1451
                env.juju_home = juju_home
1657.1.1 by Martin Packman
Work in progress network safe endpoint binding test
1452
        self.excluded_spaces = set(self.reserved_spaces)
657.1.7 by Aaron Bentley
Clean-up.
1453
1363.6.4 by Aaron Bentley
Store full path and version on Juju2Backend.
1454
    @property
1455
    def version(self):
1456
        return self._backend.version
1457
1458
    @property
1459
    def full_path(self):
1460
        return self._backend.full_path
1461
1363.6.5 by Aaron Bentley
Store feature flags on backend.
1462
    @property
1463
    def feature_flags(self):
1464
        return self._backend.feature_flags
1465
1466
    @feature_flags.setter
1467
    def feature_flags(self, feature_flags):
1468
        self._backend.feature_flags = feature_flags
1469
1363.7.4 by Aaron Bentley
Move debug to backend.
1470
    @property
1471
    def debug(self):
1472
        return self._backend.debug
1473
1363.8.4 by Aaron Bentley
Move model_name to EnvJujuClient.
1474
    @property
1475
    def model_name(self):
1476
        return self.env.environment
1477
1044.1.6 by Aaron Bentley
Use per-client JUJU_HOME.
1478
    def _shell_environ(self):
653 by Aaron Bentley
Add assess-bootstrap.
1479
        """Generate a suitable shell environment.
1480
1481
        Juju's directory must be in the PATH to support plugins.
1482
        """
1363.7.1 by Aaron Bentley
Implement _shell_environ on backend.
1483
        return self._backend.shell_environ(self.used_feature_flags,
1484
                                           self.env.juju_home)
652 by Aaron Bentley
Insert path-to-juju in PATH, fix test_assess_recovery
1485
1657.1.1 by Martin Packman
Work in progress network safe endpoint binding test
1486
    def use_reserved_spaces(self, spaces):
1487
        """Allow machines in given spaces to be allocated and used."""
1488
        if not self.reserved_spaces.issuperset(spaces):
1489
            raise ValueError('Space not reserved: {}'.format(spaces))
1490
        self.excluded_spaces.difference_update(spaces)
1491
1044.1.1 by Aaron Bentley
Use the deploy_job script everywhere, fix windows weirdness.
1492
    def add_ssh_machines(self, machines):
1256.1.1 by Aaron Bentley
Retry initial failed add-machine if needed.
1493
        for count, machine in enumerate(machines):
1494
            try:
1495
                self.juju('add-machine', ('ssh:' + machine,))
1496
            except subprocess.CalledProcessError:
1497
                if count != 0:
1498
                    raise
1499
                logging.warning('add-machine failed.  Will retry.')
1500
                pause(30)
1501
                self.juju('add-machine', ('ssh:' + machine,))
1044.1.1 by Aaron Bentley
Use the deploy_job script everywhere, fix windows weirdness.
1502
1260.2.5 by Aaron Bentley
Split up cloud and region determination.
1503
    @staticmethod
1504
    def get_cloud_region(cloud, region):
1260.2.4 by Aaron Bentley
Implement openstack + maas support.
1505
        if region is None:
1506
            return cloud
1507
        return '{}/{}'.format(cloud, region)
1508
1614.1.21 by Andrew Beach
New argument on get_bootstrap_args. New helpers in assess_bootstrap.py and almost finished the tests for them.
1509
    def get_bootstrap_args(
1510
            self, upload_tools, config_filename, bootstrap_series=None,
1511
            credential=None, auto_upgrade=False, metadata_source=None,
1670.1.1 by Martin Packman
Support --no-gui flag to juju bootstrap
1512
            to=None, no_gui=False, agent_version=None):
1260.2.4 by Aaron Bentley
Implement openstack + maas support.
1513
        """Return the bootstrap arguments for the substrate."""
1657.1.1 by Martin Packman
Work in progress network safe endpoint binding test
1514
        constraints = self._get_substrate_constraints()
1260.2.24 by Aaron Bentley
Move cloud/location to JujuData, add tests, fix lint.
1515
        cloud_region = self.get_cloud_region(self.env.get_cloud(),
1516
                                             self.env.get_region())
1646.1.1 by Christopher Lee
Update order of bootstrap args for 2.0+ (RC clients included, w/ test). Updated tests.
1517
        # Note cloud_region before controller name
1518
        args = ['--constraints', constraints,
1519
                cloud_region,
1520
                self.env.environment,
1521
                '--config', config_filename,
1315.2.2 by Aaron Bentley
Use --default-model in bootstrap.
1522
                '--default-model', self.env.environment]
1614.1.24 by Andrew Beach
Added checks to make sure the versions that don't support the new bootstrap arguments don't get them.
1523
        if upload_tools:
1614.1.25 by Andrew Beach
Last bit of pollish.
1524
            if agent_version is not None:
1525
                raise ValueError(
1526
                    'agent-version may not be given with upload-tools.')
1260.2.6 by Aaron Bentley
Introduce JujuData, remove workarounds for cloud-credentials.
1527
            args.insert(0, '--upload-tools')
1614.1.25 by Andrew Beach
Last bit of pollish.
1528
        else:
1529
            if agent_version is None:
1530
                agent_version = self.get_matching_agent_version()
1614.1.21 by Andrew Beach
New argument on get_bootstrap_args. New helpers in assess_bootstrap.py and almost finished the tests for them.
1531
            args.extend(['--agent-version', agent_version])
1242.3.1 by Aaron Bentley
Update get_bootstrap_args.
1532
        if bootstrap_series is not None:
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
1533
            args.extend(['--bootstrap-series', bootstrap_series])
1579.1.6 by Andrew Beach
Added credential argument to EnvJujuClient.bootstrap and EnvJujuClient.get_bootstrap_args.
1534
        if credential is not None:
1535
            args.extend(['--credential', credential])
1614.1.2 by Andrew Beach
Added three more arguments and tests for EnvJujuClient.bootstrap. Went for the control method. I hope we don't end up testing every argument this way.
1536
        if metadata_source is not None:
1537
            args.extend(['--metadata-source', metadata_source])
1538
        if auto_upgrade:
1539
            args.append('--auto-upgrade')
1540
        if to is not None:
1541
            args.extend(['--to', to])
1670.1.1 by Martin Packman
Support --no-gui flag to juju bootstrap
1542
        if no_gui:
1543
            args.append('--no-gui')
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
1544
        return tuple(args)
807.1.2 by Aaron Bentley
Use async bootstraps for industrial testing.
1545
1363.4.4 by Aaron Bentley
Rename create_model to add_model.
1546
    def add_model(self, env):
1606.1.1 by Aaron Bentley
Update docs.
1547
        """Add a model to this model's controller and return its client.
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
1548
1606.1.1 by Aaron Bentley
Update docs.
1549
        :param env: Class representing the new model/environment."""
1363.10.1 by Aaron Bentley
Remove cruft from _backing_state.
1550
        model_client = self.clone(env)
1551
        with model_client._bootstrap_config() as config_file:
1363.4.4 by Aaron Bentley
Rename create_model to add_model.
1552
            self._add_model(env.environment, config_file)
1363.10.1 by Aaron Bentley
Remove cruft from _backing_state.
1553
        return model_client
1363.4.1 by Aaron Bentley
Re-jigger model configuration.
1554
1555
    def make_model_config(self):
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
1556
        config_dict = make_safe_config(self)
1466.1.1 by Aaron Bentley
Update make_model_config to use agent-metadata-url.
1557
        agent_metadata_url = config_dict.pop('tools-metadata-url', None)
1558
        if agent_metadata_url is not None:
1559
            config_dict.setdefault('agent-metadata-url', agent_metadata_url)
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
1560
        # Strip unneeded variables.
1363.4.1 by Aaron Bentley
Re-jigger model configuration.
1561
        return dict((k, v) for k, v in config_dict.items() if k not in {
1260.2.26 by Aaron Bentley
Test _bootstrap_config.
1562
            'access-key',
1325.1.1 by Aaron Bentley
Exclude admin-secret from configs.
1563
            'admin-secret',
1260.2.26 by Aaron Bentley
Test _bootstrap_config.
1564
            'application-id',
1565
            'application-password',
1566
            'auth-url',
1280.1.1 by Aaron Bentley
Supply bootstrap host as manual region.
1567
            'bootstrap-host',
1260.2.26 by Aaron Bentley
Test _bootstrap_config.
1568
            'client-email',
1569
            'client-id',
1570
            'control-bucket',
1571
            'location',
1572
            'maas-oauth',
1573
            'maas-server',
1397.1.2 by Aaron Bentley
Strip old-azure credentials from bootstrap config.
1574
            'management-certificate',
1575
            'management-subscription-id',
1260.2.26 by Aaron Bentley
Test _bootstrap_config.
1576
            'manta-key-id',
1577
            'manta-user',
1578
            'name',
1579
            'password',
1580
            'private-key',
1581
            'region',
1582
            'sdc-key-id',
1583
            'sdc-url',
1584
            'sdc-user',
1585
            'secret-key',
1586
            'storage-account-name',
1587
            'subscription-id',
1588
            'tenant-id',
1589
            'tenant-name',
1590
            'type',
1591
            'username',
1306.1.1 by Curtis Hovey
Save spike to get leader and followers.
1592
        })
1363.4.1 by Aaron Bentley
Re-jigger model configuration.
1593
1594
    @contextmanager
1595
    def _bootstrap_config(self):
1596
        with temp_yaml_file(self.make_model_config()) as config_filename:
1260.2.11 by Aaron Bentley
Get jujupy tests passing, remove region for manual.
1597
            yield config_filename
1598
1315.2.23 by Aaron Bentley
Introduce set_model_name, update tests, check controller on bootstrap.
1599
    def _check_bootstrap(self):
1600
        if self.env.environment != self.env.controller.name:
1601
            raise AssertionError(
1602
                'Controller and environment names should not vary (yet)')
1603
1522.2.3 by Christopher Lee
Tie new changes to beta15 and above.
1604
    def update_user_name(self):
1631.2.2 by Christopher Lee
Further removal of @local, affecting assess_resources.
1605
        self.env.user_name = 'admin'
1522.2.3 by Christopher Lee
Tie new changes to beta15 and above.
1606
1579.1.6 by Andrew Beach
Added credential argument to EnvJujuClient.bootstrap and EnvJujuClient.get_bootstrap_args.
1607
    def bootstrap(self, upload_tools=False, bootstrap_series=None,
1614.1.2 by Andrew Beach
Added three more arguments and tests for EnvJujuClient.bootstrap. Went for the control method. I hope we don't end up testing every argument this way.
1608
                  credential=None, auto_upgrade=False, metadata_source=None,
1670.1.1 by Martin Packman
Support --no-gui flag to juju bootstrap
1609
                  to=None, no_gui=False, agent_version=None):
1260.2.11 by Aaron Bentley
Get jujupy tests passing, remove region for manual.
1610
        """Bootstrap a controller."""
1315.2.23 by Aaron Bentley
Introduce set_model_name, update tests, check controller on bootstrap.
1611
        self._check_bootstrap()
1260.2.11 by Aaron Bentley
Get jujupy tests passing, remove region for manual.
1612
        with self._bootstrap_config() as config_filename:
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
1613
            args = self.get_bootstrap_args(
1614.1.2 by Andrew Beach
Added three more arguments and tests for EnvJujuClient.bootstrap. Went for the control method. I hope we don't end up testing every argument this way.
1614
                upload_tools, config_filename, bootstrap_series, credential,
1670.1.1 by Martin Packman
Support --no-gui flag to juju bootstrap
1615
                auto_upgrade, metadata_source, to, no_gui, agent_version)
1522.2.3 by Christopher Lee
Tie new changes to beta15 and above.
1616
            self.update_user_name()
1260.2.1 by Aaron Bentley
Spike to support cloud-credentials.
1617
            self.juju('bootstrap', args, include_e=False)
1618
807.1.2 by Aaron Bentley
Use async bootstraps for industrial testing.
1619
    @contextmanager
1614.1.2 by Andrew Beach
Added three more arguments and tests for EnvJujuClient.bootstrap. Went for the control method. I hope we don't end up testing every argument this way.
1620
    def bootstrap_async(self, upload_tools=False, bootstrap_series=None,
1670.1.1 by Martin Packman
Support --no-gui flag to juju bootstrap
1621
                        auto_upgrade=False, metadata_source=None, to=None,
1622
                        no_gui=False):
1315.2.23 by Aaron Bentley
Introduce set_model_name, update tests, check controller on bootstrap.
1623
        self._check_bootstrap()
1260.2.11 by Aaron Bentley
Get jujupy tests passing, remove region for manual.
1624
        with self._bootstrap_config() as config_filename:
1625
            args = self.get_bootstrap_args(
1614.1.2 by Andrew Beach
Added three more arguments and tests for EnvJujuClient.bootstrap. Went for the control method. I hope we don't end up testing every argument this way.
1626
                upload_tools, config_filename, bootstrap_series, None,
1670.1.1 by Martin Packman
Support --no-gui flag to juju bootstrap
1627
                auto_upgrade, metadata_source, to, no_gui)
1522.2.3 by Christopher Lee
Tie new changes to beta15 and above.
1628
            self.update_user_name()
1260.2.20 by Aaron Bentley
Get all tests passing.
1629
            with self.juju_async('bootstrap', args, include_e=False):
1260.2.11 by Aaron Bentley
Get jujupy tests passing, remove region for manual.
1630
                yield
1631
                log.info('Waiting for bootstrap of {}.'.format(
1632
                    self.env.environment))
807.1.2 by Aaron Bentley
Use async bootstraps for industrial testing.
1633
1363.4.4 by Aaron Bentley
Rename create_model to add_model.
1634
    def _add_model(self, model_name, config_file):
1711.3.3 by Aaron Bentley
Let controller decide whether to supply cloud/region and credentials.
1635
        explicit_region = self.env.controller.explicit_region
1636
        if explicit_region:
1637
            credential_name = self.env.get_cloud_credentials_item()[0]
1638
            cloud_region = self.get_cloud_region(self.env.get_cloud(),
1639
                                                 self.env.get_region())
1711.3.7 by Aaron Bentley
Fix add-model argument order.
1640
            region_args = (cloud_region, '--credential', credential_name)
1711.3.3 by Aaron Bentley
Let controller decide whether to supply cloud/region and credentials.
1641
        else:
1642
            region_args = ()
1711.3.7 by Aaron Bentley
Fix add-model argument order.
1643
        self.controller_juju('add-model', (model_name,) + region_args + (
1644
            '--config', config_file))
1162.2.2 by Aaron Bentley
Switch to direct BootstrapManager.
1645
1221.1.15 by Aaron Bentley
Implement and use destroy_model for destroying models.
1646
    def destroy_model(self):
1210.1.1 by Aaron Bentley
Destroy environment without --force if possible.
1647
        exit_status = self.juju(
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
1648
            'destroy-model', (self.env.environment, '-y',),
1535.1.4 by Curtis Hovey
Extracted get_teardown_timeout.
1649
            include_e=False, timeout=get_teardown_timeout(self))
1210.1.1 by Aaron Bentley
Destroy environment without --force if possible.
1650
        return exit_status
657.1.1 by Aaron Bentley
Extract EnvJujuClient from JujuClientDevel.
1651
1162.1.1 by Aaron Bentley
Abstract jes support, make enable_jes optional in assess_jes.
1652
    def kill_controller(self):
1688.1.16 by Andrew Beach
Updated doc-strings.
1653
        """Kill a controller and its models. Hard kill option.
1654
1655
        :return: Subprocess's exit code."""
1685.1.2 by Andrew Beach
Updated the tests for kill_controller and destroy_environment.
1656
        return self.juju(
1657
            'kill-controller', (self.env.controller.name, '-y'),
1535.1.4 by Curtis Hovey
Extracted get_teardown_timeout.
1658
            include_e=False, check=False, timeout=get_teardown_timeout(self))
1162.1.1 by Aaron Bentley
Abstract jes support, make enable_jes optional in assess_jes.
1659
1688.1.15 by Andrew Beach
Added --destroy-all-models flag to destroy-controller.
1660
    def destroy_controller(self, all_models=False):
1688.1.16 by Andrew Beach
Updated doc-strings.
1661
        """Destroy a controller and its models. Soft kill option.
1662
1663
        :param all_models: If true will attempt to destroy all the
1664
            controller's models as well.
1665
        :raises: subprocess.CalledProcessError if the operation fails."""
1688.1.15 by Andrew Beach
Added --destroy-all-models flag to destroy-controller.
1666
        args = (self.env.controller.name, '-y')
1667
        if all_models:
1668
            args += ('--destroy-all-models',)
1669
        return self.juju('destroy-controller', args, include_e=False,
1670
                         timeout=get_teardown_timeout(self))
1685.1.4 by Andrew Beach
Added the soft kill destroy-controller.
1671
1688.1.13 by Andrew Beach
Fake Merge.
1672
    def tear_down(self):
1673
        """Tear down the client as cleanly as possible.
1674
1675
        Attempts to use the soft method destroy_controller, if that fails
1688.1.15 by Andrew Beach
Added --destroy-all-models flag to destroy-controller.
1676
        it will use the hard kill_controller and raise an error."""
1688.1.13 by Andrew Beach
Fake Merge.
1677
        try:
1688.1.15 by Andrew Beach
Added --destroy-all-models flag to destroy-controller.
1678
            self.destroy_controller(all_models=True)
1688.1.13 by Andrew Beach
Fake Merge.
1679
        except subprocess.CalledProcessError:
1680
            logging.warning('tear_down destroy-controller failed')
1681
            retval = self.kill_controller()
1682
            message = 'tear_down kill-controller result={}'.format(retval)
1683
            if retval == 0:
1684
                logging.info(message)
1685
            else:
1686
                logging.warning(message)
1687
            raise
1688
657.1.1 by Aaron Bentley
Extract EnvJujuClient from JujuClientDevel.
1689
    def get_juju_output(self, command, *args, **kwargs):
953.3.13 by Nate Finch
more code review changes
1690
        """Call a juju command and return the output.
1691
1692
        Sub process will be called as 'juju <command> <args> <kwargs>'. Note
1693
        that <command> may be a space delimited list of arguments. The -e
953.3.15 by Aaron Bentley
Fix lint.
1694
        <environment> flag will be placed after <command> and before args.
953.3.13 by Nate Finch
more code review changes
1695
        """
1363.8.8 by Aaron Bentley
Move juju_async to backend.
1696
        model = self._cmd_model(kwargs.get('include_e', True),
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1697
                                kwargs.get('controller', False))
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
1698
        pass_kwargs = dict(
1699
            (k, kwargs[k]) for k in kwargs if k in ['timeout', 'merge_stderr'])
1477.2.6 by Leo Zhang
New updates after review
1700
        return self._backend.get_juju_output(
1701
            command, args, self.used_feature_flags, self.env.juju_home,
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
1702
            model, user_name=self.env.user_name, **pass_kwargs)
19.1.18 by Aaron Bentley
Provide destroy-environment script to simplify code.
1703
1221.5.15 by Aaron Bentley
Use 'show-status' instead of 'status'.
1704
    def show_status(self):
1705
        """Print the status to output."""
1246.1.4 by Curtis Hovey
Safe print yaml status to ensure continuity.
1706
        self.juju(self._show_status, ('--format', 'yaml'))
1221.5.15 by Aaron Bentley
Use 'show-status' instead of 'status'.
1707
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1708
    def get_status(self, timeout=60, raw=False, controller=False, *args):
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
1709
        """Get the current status as a dict."""
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
1710
        # GZ 2015-12-16: Pass remaining timeout into get_juju_output call.
419.1.13 by Aaron Bentley
Allow 10 minutes for status to start working again.
1711
        for ignored in until_timeout(timeout):
331.1.1 by Aaron Bentley
juju status retries on error for 30 seconds.
1712
            try:
979.3.5 by Horacio Durán
Addressed curtis observations.
1713
                if raw:
1221.5.15 by Aaron Bentley
Use 'show-status' instead of 'status'.
1714
                    return self.get_juju_output(self._show_status, *args)
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
1715
                return self.status_class.from_text(
1246.1.1 by Curtis Hovey
Call status with --format yaml.
1716
                    self.get_juju_output(
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1717
                        self._show_status, '--format', 'yaml',
1718
                        controller=controller))
1091.5.4 by James Tunnicliffe
SSH wrapper now trying even harder to retry on connection errors.
1719
            except subprocess.CalledProcessError:
331.1.1 by Aaron Bentley
juju status retries on error for 30 seconds.
1720
                pass
1721
        raise Exception(
1091.5.4 by James Tunnicliffe
SSH wrapper now trying even harder to retry on connection errors.
1722
            'Timed out waiting for juju status to succeed')
19.1.17 by Aaron Bentley
Isolate client to support incompatible command line changes.
1723
1718.1.15 by Christopher Lee
Move show-model code into JujuEnvClient.
1724
    def show_model(self, model_name=None):
1725
        model_details = self.get_juju_output(
1726
            'show-model',
1727
            '{}:{}'.format(
1728
                self.env.controller.name, model_name or self.env.environment),
1729
            '--format', 'yaml',
1730
            include_e=False)
1731
        return yaml.safe_load(model_details)
1732
1251.1.1 by Aaron Bentley
Implement and use set_model_constraints.
1733
    @staticmethod
1734
    def _dict_as_option_strings(options):
1735
        return tuple('{}={}'.format(*item) for item in options.items())
1736
1239.1.1 by Aaron Bentley
Implement and use EnvJujuClient.set_config.
1737
    def set_config(self, service, options):
1251.1.1 by Aaron Bentley
Implement and use set_model_constraints.
1738
        option_strings = self._dict_as_option_strings(options)
1588.1.1 by Curtis Hovey
Remove set-/set- from config command.
1739
        self.juju('config', (service,) + option_strings)
1239.1.1 by Aaron Bentley
Implement and use EnvJujuClient.set_config.
1740
1221.5.11 by Aaron Bentley
Extract EnvJujuClient.get_config.
1741
    def get_config(self, service):
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
1742
        return yaml.safe_load(self.get_juju_output('config', service))
1221.5.11 by Aaron Bentley
Extract EnvJujuClient.get_config.
1743
979.2.1 by John George
Add support for checking chaos is complete, from run_chaos_monkey.py
1744
    def get_service_config(self, service, timeout=60):
1745
        for ignored in until_timeout(timeout):
1746
            try:
1221.5.11 by Aaron Bentley
Extract EnvJujuClient.get_config.
1747
                return self.get_config(service)
979.2.1 by John George
Add support for checking chaos is complete, from run_chaos_monkey.py
1748
            except subprocess.CalledProcessError:
1749
                pass
1750
        raise Exception(
1751
            'Timed out waiting for juju get %s' % (service))
1752
1251.1.1 by Aaron Bentley
Implement and use set_model_constraints.
1753
    def set_model_constraints(self, constraints):
1754
        constraint_strings = self._dict_as_option_strings(constraints)
1755
        return self.juju('set-model-constraints', constraint_strings)
1756
1243.1.1 by Aaron Bentley
Support rename of get-env/set-env => get-model-config/set-model-config.
1757
    def get_model_config(self):
1583.1.1 by Curtis Hovey
Rename get/set/unset model-config calls to match 2.0-beta18.
1758
        """Return the value of the environment's configured options."""
1542.1.1 by Christopher Lee
Add initial test for model config tree. Includes fix for get_model_config
1759
        return yaml.safe_load(
1583.1.1 by Curtis Hovey
Rename get/set/unset model-config calls to match 2.0-beta18.
1760
            self.get_juju_output('model-config', '--format', 'yaml'))
1243.1.1 by Aaron Bentley
Support rename of get-env/set-env => get-model-config/set-model-config.
1761
657.1.7 by Aaron Bentley
Clean-up.
1762
    def get_env_option(self, option):
1763
        """Return the value of the environment's configured option."""
1583.1.1 by Curtis Hovey
Rename get/set/unset model-config calls to match 2.0-beta18.
1764
        return self.get_juju_output('model-config', option)
657.1.7 by Aaron Bentley
Clean-up.
1765
1766
    def set_env_option(self, option, value):
1767
        """Set the value of the option in the environment."""
1768
        option_value = "%s=%s" % (option, value)
1583.1.1 by Curtis Hovey
Rename get/set/unset model-config calls to match 2.0-beta18.
1769
        return self.juju('model-config', (option_value,))
657.1.7 by Aaron Bentley
Clean-up.
1770
1542.1.1 by Christopher Lee
Add initial test for model config tree. Includes fix for get_model_config
1771
    def unset_env_option(self, option):
1772
        """Unset the value of the option in the environment."""
1583.1.1 by Curtis Hovey
Rename get/set/unset model-config calls to match 2.0-beta18.
1773
        return self.juju('model-config', ('--reset', option,))
1542.1.1 by Christopher Lee
Add initial test for model config tree. Includes fix for get_model_config
1774
1540.3.2 by Christopher Lee
Change tools_ to agent_
1775
    def get_agent_metadata_url(self):
1776
        return self.get_env_option(self.agent_metadata_url)
1540.3.1 by Christopher Lee
Update upgrade methods to work with juju 2.0. Upgrades controller first then any other hosted models.
1777
1540.3.2 by Christopher Lee
Change tools_ to agent_
1778
    def set_testing_agent_metadata_url(self):
1779
        url = self.get_agent_metadata_url()
880.1.17 by Aaron Bentley
Fake merge of trunk into no-environment.
1780
        if 'testing' not in url:
1781
            testing_url = url.replace('/tools', '/testing/tools')
1540.3.2 by Christopher Lee
Change tools_ to agent_
1782
            self.set_env_option(self.agent_metadata_url, testing_url)
880.1.17 by Aaron Bentley
Fake merge of trunk into no-environment.
1783
768.1.1 by Aaron Bentley
Add timeout to EnvJujuClient.destroy_environment.
1784
    def juju(self, command, args, sudo=False, check=True, include_e=True,
1044.1.6 by Aaron Bentley
Use per-client JUJU_HOME.
1785
             timeout=None, extra_env=None):
657.1.7 by Aaron Bentley
Clean-up.
1786
        """Run a command under juju for the current environment."""
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1787
        model = self._cmd_model(include_e, controller=False)
1363.8.2 by Aaron Bentley
Move juju implementation to backend.
1788
        return self._backend.juju(
1789
            command, args, self.used_feature_flags, self.env.juju_home,
1790
            model, check, timeout, extra_env)
1048.2.1 by John George
Capture duration of juju commands from the client perspective.
1791
1401.1.4 by Christopher Lee
Add JujuEnvClient.expect with test. Update assess test to use new functionality.
1792
    def expect(self, command, args=(), sudo=False, include_e=True,
1793
               timeout=None, extra_env=None):
1794
        """Return a process object that is running an interactive `command`.
1795
1796
        The interactive command ability is provided by using pexpect.
1797
1798
        :param command: String of the juju command to run.
1401.2.4 by Christopher Lee
Fix typos and incorrect docstring
1799
        :param args: Tuple containing arguments for the juju `command`.
1401.1.4 by Christopher Lee
Add JujuEnvClient.expect with test. Update assess test to use new functionality.
1800
        :param sudo: Whether to call `command` using sudo.
1801
        :param include_e: Boolean regarding supplying the juju environment to
1802
          `command`.
1803
        :param timeout: A float that, if provided, is the timeout in which the
1804
          `command` is run.
1805
1806
        :return: A pexpect.spawn object that has been called with `command` and
1807
          `args`.
1808
1809
        """
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1810
        model = self._cmd_model(include_e, controller=False)
1410.2.4 by Christopher Lee
Fix EnvJujuClient.expect to ensure it uses the provided path within envvar.
1811
        return self._backend.expect(
1812
            command, args, self.used_feature_flags, self.env.juju_home,
1813
            model, timeout, extra_env)
1401.1.4 by Christopher Lee
Add JujuEnvClient.expect with test. Update assess test to use new functionality.
1814
1315.2.22 by Aaron Bentley
Fake merge of trunk.
1815
    def controller_juju(self, command, args):
1816
        args = ('-c', self.env.controller.name) + args
1817
        return self.juju(command, args, include_e=False)
1818
1048.2.1 by John George
Capture duration of juju commands from the client perspective.
1819
    def get_juju_timings(self):
1820
        stringified_timings = {}
1363.8.1 by Aaron Bentley
Move juju_timings to backend.
1821
        for command, timings in self._backend.juju_timings.items():
1048.2.1 by John George
Capture duration of juju commands from the client perspective.
1822
            stringified_timings[' '.join(command)] = timings
1823
        return stringified_timings
657.1.7 by Aaron Bentley
Clean-up.
1824
1044.1.6 by Aaron Bentley
Use per-client JUJU_HOME.
1825
    def juju_async(self, command, args, include_e=True, timeout=None):
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
1826
        model = self._cmd_model(include_e, controller=False)
1363.8.8 by Aaron Bentley
Move juju_async to backend.
1827
        return self._backend.juju_async(command, args, self.used_feature_flags,
1828
                                        self.env.juju_home, model, timeout)
807.1.2 by Aaron Bentley
Use async bootstraps for industrial testing.
1829
1345.1.3 by Seman
Deploy charm by path.
1830
    def deploy(self, charm, repository=None, to=None, series=None,
1566.1.1 by Leo Zhang
Add assess_constraints
1831
               service=None, force=False, resource=None,
1832
               storage=None, constraints=None):
874.2.4 by Aaron Bentley
Initial upgrade-charm test.
1833
        args = [charm]
1389.1.1 by Aaron Bentley
Use a timeout of 60 for lxd removal.
1834
        if service is not None:
1835
            args.extend([service])
1014.2.1 by John George
background_chaos WIP
1836
        if to is not None:
1837
            args.extend(['--to', to])
1337.1.1 by Reed O'Brien
This adds the `--series` flag to the deploy client wrapper. It allows
1838
        if series is not None:
1839
            args.extend(['--series', series])
1344.3.3 by Nicholas Skaggs
shell in place
1840
        if force is True:
1841
            args.extend(['--force'])
1410.1.1 by Seman
Added CI test for deploying a charm with resources.
1842
        if resource is not None:
1843
            args.extend(['--resource', resource])
1426.1.1 by Leo Zhang
Add assess_storage
1844
        if storage is not None:
1845
            args.extend(['--storage', storage])
1566.1.1 by Leo Zhang
Add assess_constraints
1846
        if constraints is not None:
1847
            args.extend(['--constraints', constraints])
874.2.4 by Aaron Bentley
Initial upgrade-charm test.
1848
        return self.juju('deploy', tuple(args))
650.1.12 by Aaron Bentley
Rename to assess-foreign, update to use EnvJujuClient.
1849
1410.1.1 by Seman
Added CI test for deploying a charm with resources.
1850
    def attach(self, service, resource):
1851
        args = (service, resource)
1852
        return self.juju('attach', args)
1853
1854
    def list_resources(self, service_or_unit, details=True):
1855
        args = ('--format', 'yaml', service_or_unit)
1856
        if details:
1857
            args = args + ('--details',)
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
1858
        return yaml.safe_load(self.get_juju_output('list-resources', *args))
1410.1.1 by Seman
Added CI test for deploying a charm with resources.
1859
1417.1.1 by Seman
Added more resource CI tests.
1860
    def wait_for_resource(self, resource_id, service_or_unit, timeout=60):
1861
        log.info('Waiting for resource. Resource id:{}'.format(resource_id))
1575.1.3 by Aaron Bentley
Handle soft deadlines for wait_for_resource.
1862
        with self.check_timeouts():
1863
            with self.ignore_soft_deadline():
1864
                for _ in until_timeout(timeout):
1865
                    resources_dict = self.list_resources(service_or_unit)
1866
                    resources = resources_dict['resources']
1867
                    for resource in resources:
1868
                        if resource['expected']['resourceid'] == resource_id:
1869
                            if (resource['expected']['fingerprint'] ==
1870
                                    resource['unit']['fingerprint']):
1871
                                return
1872
                    time.sleep(.1)
1873
                raise JujuResourceTimeout(
1874
                    'Timeout waiting for a resource to be downloaded. '
1875
                    'ResourceId: {} Service or Unit: {} Timeout: {}'.format(
1876
                        resource_id, service_or_unit, timeout))
1417.1.1 by Seman
Added more resource CI tests.
1877
1369.1.1 by Aaron Bentley
Implement upgrade_charm, switch industrial_test to it.
1878
    def upgrade_charm(self, service, charm_path=None):
1879
        args = (service,)
1880
        if charm_path is not None:
1881
            args = args + ('--path', charm_path)
1882
        self.juju('upgrade-charm', args)
1883
1221.4.1 by Aaron Bentley
Extract destroy-service to a method.
1884
    def remove_service(self, service):
1420.2.1 by Aaron Bentley
Update tests to use applications, not services.
1885
        self.juju('remove-application', (service,))
1221.4.1 by Aaron Bentley
Extract destroy-service to a method.
1886
1341.2.5 by Aaron Bentley
Allow {container} expansion in bundle names.
1887
    @classmethod
1888
    def format_bundle(cls, bundle_template):
1889
        return bundle_template.format(container=cls.preferred_container())
1890
1891
    def deploy_bundle(self, bundle_template, timeout=_DEFAULT_BUNDLE_TIMEOUT):
1259.2.1 by Martin Packman
Add flag to run_deployer to use juju 2.0 bundle deployment
1892
        """Deploy bundle using native juju 2.0 deploy command."""
1341.2.5 by Aaron Bentley
Allow {container} expansion in bundle names.
1893
        bundle = self.format_bundle(bundle_template)
1259.2.1 by Martin Packman
Add flag to run_deployer to use juju 2.0 bundle deployment
1894
        self.juju('deploy', bundle, timeout=timeout)
1895
1341.2.5 by Aaron Bentley
Allow {container} expansion in bundle names.
1896
    def deployer(self, bundle_template, name=None, deploy_delay=10,
1897
                 timeout=3600):
1460.1.2 by Aaron Bentley
Updates from review
1898
        """Deploy a bundle using deployer."""
1341.2.5 by Aaron Bentley
Allow {container} expansion in bundle names.
1899
        bundle = self.format_bundle(bundle_template)
884 by John George
Add juju-deployer test support.
1900
        args = (
1901
            '--debug',
1204.1.2 by John George
Pass deployer timeout values as function parameters.
1902
            '--deploy-delay', str(deploy_delay),
1903
            '--timeout', str(timeout),
884 by John George
Add juju-deployer test support.
1904
            '--config', bundle,
1905
        )
953.2.1 by John George
Support taking a bundle name in addition to the bundle config file path.
1906
        if name:
1907
            args += (name,)
1460.1.1 by Aaron Bentley
Remove local. prefix from deployer after beta-8
1908
        e_arg = ('-e', '{}:{}'.format(
1363.1.1 by Seman
Updated Deployer to support Juju 2.X.
1909
            self.env.controller.name, self.env.environment))
1910
        args = e_arg + args
1911
        self.juju('deployer', args, self.env.needs_sudo(), include_e=False)
884 by John George
Add juju-deployer test support.
1912
1680.1.1 by Martin Packman
Add environment variable to disable space constraint on maas
1913
    @staticmethod
1914
    def _maas_spaces_enabled():
1915
        return not os.environ.get("JUJU_CI_SPACELESSNESS")
1916
1260.2.30 by Aaron Bentley
Updates from review
1917
    def _get_substrate_constraints(self):
1513.2.1 by Martin Packman
Remove arch=amd64 constraint for maas deploys
1918
        if self.env.joyent:
1260.2.30 by Aaron Bentley
Updates from review
1919
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1920
            return 'mem=2G cpu-cores=1'
1680.1.1 by Martin Packman
Add environment variable to disable space constraint on maas
1921
        elif self.env.maas and self._maas_spaces_enabled():
1657.1.1 by Martin Packman
Work in progress network safe endpoint binding test
1922
            # For now only maas support spaces in a meaningful way.
1675.1.1 by Martin Packman
Fix test failures, lint, and bundle issue from endpoint bindings
1923
            return 'mem=2G spaces={}'.format(','.join(
1657.1.1 by Martin Packman
Work in progress network safe endpoint binding test
1924
                '^' + space for space in sorted(self.excluded_spaces)))
1260.2.30 by Aaron Bentley
Updates from review
1925
        else:
1926
            return 'mem=2G'
1927
1341.2.5 by Aaron Bentley
Allow {container} expansion in bundle names.
1928
    def quickstart(self, bundle_template, upload_tools=False):
796.2.2 by John George
Call quickstart from EnvJujuClient, with constraints
1929
        """quickstart, using sudo if necessary."""
1341.2.5 by Aaron Bentley
Allow {container} expansion in bundle names.
1930
        bundle = self.format_bundle(bundle_template)
1513.2.1 by Martin Packman
Remove arch=amd64 constraint for maas deploys
1931
        constraints = 'mem=2G'
796.2.2 by John George
Call quickstart from EnvJujuClient, with constraints
1932
        args = ('--constraints', constraints)
1933
        if upload_tools:
1934
            args = ('--upload-tools',) + args
1935
        args = args + ('--no-browser', bundle,)
966.2.2 by Curtis Hovey
pass extra_env={'JUJU': None} with the quickstart command.
1936
        self.juju('quickstart', args, self.env.needs_sudo(),
1937
                  extra_env={'JUJU': self.full_path})
796.2.2 by John George
Call quickstart from EnvJujuClient, with constraints
1938
874.2.4 by Aaron Bentley
Initial upgrade-charm test.
1939
    def status_until(self, timeout, start=None):
874.2.8 by Aaron Bentley
Add docs, simplify get_unit.
1940
        """Call and yield status until the timeout is reached.
1941
1942
        Status will always be yielded once before checking the timeout.
1943
1944
        This is intended for implementing things like wait_for_started.
1945
1946
        :param timeout: The number of seconds to wait before timing out.
1947
        :param start: If supplied, the time to count from when determining
1948
            timeout.
1949
        """
1575.1.1 by Aaron Bentley
Soft timeouts for status_until.
1950
        with self.check_timeouts():
1951
            with self.ignore_soft_deadline():
1952
                yield self.get_status()
1953
                for remaining in until_timeout(timeout, start=start):
1954
                    yield self.get_status()
874.2.4 by Aaron Bentley
Initial upgrade-charm test.
1955
1196.1.6 by Martin Packman
Changes for review by abentley
1956
    def _wait_for_status(self, reporter, translate, exc_type=StatusNotMet,
1196.1.4 by Martin Packman
Finish increased sanity on deployer tests
1957
                         timeout=1200, start=None):
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
1958
        """Wait till status reaches an expected state with pretty reporting.
1959
1960
        Always tries to get status at least once. Each status call has an
1961
        internal timeout of 60 seconds. This is independent of the timeout for
1962
        the whole wait, note this means this function may be overrun.
1963
1964
        :param reporter: A GroupReporter instance for output.
1965
        :param translate: A callable that takes status to make states dict.
1196.1.6 by Martin Packman
Changes for review by abentley
1966
        :param exc_type: Optional StatusNotMet subclass to raise on timeout.
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
1967
        :param timeout: Optional number of seconds to wait before timing out.
1968
        :param start: Optional time to count from when determining timeout.
1969
        """
1970
        status = None
1971
        try:
1564.3.7 by Aaron Bentley
Ensure check_timeouts is called after _wait_for_status.
1972
            with self.check_timeouts():
1973
                with self.ignore_soft_deadline():
1974
                    for _ in chain([None],
1975
                                   until_timeout(timeout, start=start)):
1976
                        try:
1977
                            status = self.get_status()
1978
                        except CannotConnectEnv:
1979
                            log.info(
1980
                                'Suppressing "Unable to connect to'
1981
                                ' environment"')
1982
                            continue
1983
                        states = translate(status)
1984
                        if states is None:
1985
                            break
1726.4.1 by Andrew Beach
Tried actually using check_for_errors which highlighted some errors. I had to change a bunch of existing errors just to get meaningful errors out of them.
1986
                        status.raise_highest_error(ignore_recoverable=True)
1564.3.7 by Aaron Bentley
Ensure check_timeouts is called after _wait_for_status.
1987
                        reporter.update(states)
1988
                    else:
1989
                        if status is not None:
1990
                            log.error(status.status_text)
1726.4.1 by Andrew Beach
Tried actually using check_for_errors which highlighted some errors. I had to change a bunch of existing errors just to get meaningful errors out of them.
1991
                            status.raise_highest_error(
1992
                                ignore_recoverable=False)
1564.3.7 by Aaron Bentley
Ensure check_timeouts is called after _wait_for_status.
1993
                        raise exc_type(self.env.environment, status)
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
1994
        finally:
1995
            reporter.finish()
1996
        return status
1997
751.1.1 by Aaron Bentley
Give equal time to old and new clients in DeployManyAttempt
1998
    def wait_for_started(self, timeout=1200, start=None):
657.1.5 by Aaron Bentley
Move wait_for_started to EnvJujuClient.
1999
        """Wait until all unit/machine agents are 'started'."""
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
2000
        reporter = GroupReporter(sys.stdout, 'started')
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2001
        return self._wait_for_status(
2002
            reporter, Status.check_agents_started, AgentsNotStarted,
2003
            timeout=timeout, start=start)
657.1.5 by Aaron Bentley
Move wait_for_started to EnvJujuClient.
2004
966.1.10 by John George
Check that subordinate unit count matches service unit count in wait_for_subordinate_units.
2005
    def wait_for_subordinate_units(self, service, unit_prefix, timeout=1200,
2006
                                   start=None):
966.1.12 by John George
Verify subordinate units are started in wait_for_subordinate_units and add the reporter.
2007
        """Wait until all service units have a started subordinate with
966.1.10 by John George
Check that subordinate unit count matches service unit count in wait_for_subordinate_units.
2008
        unit_prefix."""
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2009
        def status_to_subordinate_states(status):
2010
            service_unit_count = status.get_service_unit_count(service)
2011
            subordinate_unit_count = 0
2012
            unit_states = defaultdict(list)
2013
            for name, unit in status.service_subordinate_units(service):
2014
                if name.startswith(unit_prefix + '/'):
2015
                    subordinate_unit_count += 1
1246.1.2 by Curtis Hovey
Added coalesce_agent_status to handle machine agent-state and unit agent-status.
2016
                    unit_states[coalesce_agent_status(unit)].append(name)
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2017
            if (subordinate_unit_count == service_unit_count and
1246.1.2 by Curtis Hovey
Added coalesce_agent_status to handle machine agent-state and unit agent-status.
2018
                    set(unit_states.keys()).issubset(AGENTS_READY)):
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2019
                return None
2020
            return unit_states
966.1.12 by John George
Verify subordinate units are started in wait_for_subordinate_units and add the reporter.
2021
        reporter = GroupReporter(sys.stdout, 'started')
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2022
        self._wait_for_status(
2023
            reporter, status_to_subordinate_states, AgentsNotStarted,
2024
            timeout=timeout, start=start)
966.1.9 by John George
Add wait_for_subordinate_unit to jujupy.py.
2025
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2026
    def wait_for_version(self, version, timeout=300, start=None):
2027
        def status_to_version(status):
2028
            versions = status.get_agent_versions()
2029
            if versions.keys() == [version]:
2030
                return None
2031
            return versions
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
2032
        reporter = GroupReporter(sys.stdout, version)
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2033
        self._wait_for_status(reporter, status_to_version, VersionsNotUpdated,
1196.1.4 by Martin Packman
Finish increased sanity on deployer tests
2034
                              timeout=timeout, start=start)
657.1.6 by Aaron Bentley
Move wait_for_version to EnvJujuClient.
2035
1306.1.21 by Curtis Hovey
added list-controllers and list-models support for 2.x.
2036
    def list_models(self):
2037
        """List the models registered with the current controller."""
1315.2.22 by Aaron Bentley
Fake merge of trunk.
2038
        self.controller_juju('list-models', ())
1306.1.21 by Curtis Hovey
added list-controllers and list-models support for 2.x.
2039
2040
    def get_models(self):
1593.2.9 by Curtis Hovey
Require get_model() to be fast.
2041
        """return a models dict with a 'models': [] key-value pair.
2042
2043
        The server has 120 seconds to respond because this method is called
2044
        often when tearing down a controller-less deployment.
2045
        """
1306.1.21 by Curtis Hovey
added list-controllers and list-models support for 2.x.
2046
        output = self.get_juju_output(
1427.1.1 by Christopher Lee
Update commands to use <controller>:<model> format. Fix arg to -c to be controller name.
2047
            'list-models', '-c', self.env.controller.name, '--format', 'yaml',
1593.2.9 by Curtis Hovey
Require get_model() to be fast.
2048
            include_e=False, timeout=120)
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
2049
        models = yaml.safe_load(output)
1306.1.21 by Curtis Hovey
added list-controllers and list-models support for 2.x.
2050
        return models
2051
1315.2.8 by Aaron Bentley
Dump logs for all models, including on JES.
2052
    def _get_models(self):
2053
        """return a list of model dicts."""
2054
        return self.get_models()['models']
2055
2056
    def iter_model_clients(self):
1318.2.1 by Aaron Bentley
Unify cloning code, add docs.
2057
        """Iterate through all the models that share this model's controller.
2058
2059
        Works only if JES is enabled.
2060
        """
1315.2.8 by Aaron Bentley
Dump logs for all models, including on JES.
2061
        models = self._get_models()
2062
        if not models:
2063
            yield self
1612.2.16 by Christopher Lee
Remove bad changes to iter_client_models, Remove unneeded test. Fix existing test.
2064
        for model in models:
2065
            yield self._acquire_model_client(model['name'])
1315.2.8 by Aaron Bentley
Dump logs for all models, including on JES.
2066
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
2067
    def get_controller_model_name(self):
2068
        """Return the name of the 'controller' model.
1306.1.21 by Curtis Hovey
added list-controllers and list-models support for 2.x.
2069
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
2070
        Return the name of the environment when an 'controller' model does
1306.1.21 by Curtis Hovey
added list-controllers and list-models support for 2.x.
2071
        not exist.
2072
        """
1423.1.1 by Curtis Hovey
Rename 'admin' model to 'controller', preserve old name in EnvJujuClient2B7.
2073
        return 'controller'
1315.2.3 by Aaron Bentley
Use admin client to get bootstrap host.
2074
1318.2.1 by Aaron Bentley
Unify cloning code, add docs.
2075
    def _acquire_model_client(self, name):
2076
        """Get a client for a model with the supplied name.
2077
2078
        If the name matches self, self is used.  Otherwise, a clone is used.
2079
        """
2080
        if name == self.env.environment:
2081
            return self
2082
        else:
2083
            env = self.env.clone(model_name=name)
2084
            return self.clone(env=env)
2085
1491.2.20 by Christopher Lee
Add test for hosted models.
2086
    def get_model_uuid(self):
2087
        name = self.env.environment
1556.1.2 by Curtis Hovey
Always pass the controller:model to show-model when getting uuid.
2088
        model = self._cmd_model(True, False)
1556.1.1 by Curtis Hovey
Don't pass -m to show-model.
2089
        output_yaml = self.get_juju_output(
1556.1.2 by Curtis Hovey
Always pass the controller:model to show-model when getting uuid.
2090
            'show-model', '--format', 'yaml', model, include_e=False)
1491.2.20 by Christopher Lee
Add test for hosted models.
2091
        output = yaml.safe_load(output_yaml)
2092
        return output[name]['model-uuid']
2093
1491.2.14 by Christopher Lee
Make test pass for added get_controller_uuid
2094
    def get_controller_uuid(self):
2095
        name = self.env.controller.name
2096
        output_yaml = self.get_juju_output(
2097
            'show-controller', '--format', 'yaml', include_e=False)
2098
        output = yaml.safe_load(output_yaml)
2099
        return output[name]['details']['uuid']
2100
1576.1.1 by Christopher Lee
Add, test and use get_controller_model_uuid method.
2101
    def get_controller_model_uuid(self):
2102
        output_yaml = self.get_juju_output(
2103
            'show-model', 'controller', '--format', 'yaml', include_e=False)
2104
        output = yaml.safe_load(output_yaml)
2105
        return output['controller']['model-uuid']
2106
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
2107
    def get_controller_client(self):
2108
        """Return a client for the controller model.  May return self.
1315.2.8 by Aaron Bentley
Dump logs for all models, including on JES.
2109
1363.4.4 by Aaron Bentley
Rename create_model to add_model.
2110
        This may be inaccurate for models created using add_model
1315.2.8 by Aaron Bentley
Dump logs for all models, including on JES.
2111
        rather than bootstrap.
2112
        """
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
2113
        return self._acquire_model_client(self.get_controller_model_name())
1306.1.21 by Curtis Hovey
added list-controllers and list-models support for 2.x.
2114
2115
    def list_controllers(self):
2116
        """List the controllers."""
2117
        self.juju('list-controllers', (), include_e=False)
2118
1306.1.4 by Curtis Hovey
Added get_controller_endpoint for show-controller.
2119
    def get_controller_endpoint(self):
2120
        """Return the address of the controller leader."""
1324.1.1 by Aaron Bentley
Use controller name for show-controller.
2121
        controller = self.env.controller.name
1306.1.19 by Curtis Hovey
Fix calls to show-controller with include_e=False.
2122
        output = self.get_juju_output(
1324.1.1 by Aaron Bentley
Use controller name for show-controller.
2123
            'show-controller', controller, include_e=False)
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
2124
        info = yaml.safe_load(output)
1324.1.1 by Aaron Bentley
Use controller name for show-controller.
2125
        endpoint = info[controller]['details']['api-endpoints'][0]
1310.1.2 by Curtis Hovey
Address ipv6 split address and port.
2126
        address, port = split_address_port(endpoint)
1306.1.4 by Curtis Hovey
Added get_controller_endpoint for show-controller.
2127
        return address
2128
1306.1.6 by Curtis Hovey
Added get_controller_members.
2129
    def get_controller_members(self):
1306.1.8 by Curtis Hovey
Update docstrings.
2130
        """Return a list of Machines that are members of the controller.
1306.1.6 by Curtis Hovey
Added get_controller_members.
2131
1306.1.8 by Curtis Hovey
Update docstrings.
2132
        The first machine in the list is the leader. the remaining machines
1306.1.6 by Curtis Hovey
Added get_controller_members.
2133
        are followers in a HA relationship.
2134
        """
1310.1.3 by Curtis Hovey
Address sorting comment from review.
2135
        members = []
1306.1.6 by Curtis Hovey
Added get_controller_members.
2136
        status = self.get_status()
1315.2.1 by Aaron Bentley
Use machine id instead of machine number.
2137
        for machine_id, machine in status.iter_machines():
1306.1.6 by Curtis Hovey
Added get_controller_members.
2138
            if self.get_controller_member_status(machine):
1315.2.1 by Aaron Bentley
Use machine id instead of machine number.
2139
                members.append(Machine(machine_id, machine))
1310.1.3 by Curtis Hovey
Address sorting comment from review.
2140
        if len(members) <= 1:
2141
            return members
1306.1.6 by Curtis Hovey
Added get_controller_members.
2142
        # Search for the leader and make it the first in the list.
1310.1.3 by Curtis Hovey
Address sorting comment from review.
2143
        # If the endpoint address is not the same as the leader's dns_name,
2144
        # the members are return in the order they were discovered.
1306.1.6 by Curtis Hovey
Added get_controller_members.
2145
        endpoint = self.get_controller_endpoint()
1310.1.3 by Curtis Hovey
Address sorting comment from review.
2146
        log.debug('Controller endpoint is at {}'.format(endpoint))
2147
        members.sort(key=lambda m: m.info.get('dns-name') != endpoint)
2148
        return members
1306.1.6 by Curtis Hovey
Added get_controller_members.
2149
1306.1.7 by Curtis Hovey
Added get_controller_leader.
2150
    def get_controller_leader(self):
1306.1.8 by Curtis Hovey
Update docstrings.
2151
        """Return the controller leader Machine."""
1306.1.7 by Curtis Hovey
Added get_controller_leader.
2152
        controller_members = self.get_controller_members()
2153
        return controller_members[0]
2154
1259.1.2 by Aaron Bentley
Implement get_controller_member_status.
2155
    @staticmethod
2156
    def get_controller_member_status(info_dict):
1306.1.8 by Curtis Hovey
Update docstrings.
2157
        """Return the controller-member-status of the machine if it exists."""
1259.1.2 by Aaron Bentley
Implement get_controller_member_status.
2158
        return info_dict.get('controller-member-status')
2159
703.1.1 by Aaron Bentley
Add ensure-availability stage to industrial_test.
2160
    def wait_for_ha(self, timeout=1200):
2161
        desired_state = 'has-vote'
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
2162
        reporter = GroupReporter(sys.stdout, desired_state)
943.1.1 by Martin Packman
Use try/finally to always call finish on GroupReporter instances
2163
        try:
1564.3.8 by Aaron Bentley
Handle soft deadline in wait_for_ha.
2164
            with self.check_timeouts():
2165
                with self.ignore_soft_deadline():
1738.2.3 by Andrew Beach
Minor adjustment.
2166
                    status = None
1564.3.8 by Aaron Bentley
Handle soft deadline in wait_for_ha.
2167
                    for remaining in until_timeout(timeout):
2168
                        status = self.get_status(controller=True)
2169
                        status.check_agents_started()
2170
                        states = {}
2171
                        for machine, info in status.iter_machines():
2172
                            status = self.get_controller_member_status(info)
2173
                            if status is None:
2174
                                continue
2175
                            states.setdefault(status, []).append(machine)
2176
                        if states.keys() == [desired_state]:
2177
                            if len(states.get(desired_state, [])) >= 3:
2178
                                break
2179
                        reporter.update(states)
2180
                    else:
1738.2.1 by Andrew Beach
Added StatusNotMet exceptions for wait_for_{deploy_started,ha}.
2181
                        raise VotingNotEnabled(self.env.environment, status)
943.1.1 by Martin Packman
Use try/finally to always call finish on GroupReporter instances
2182
        finally:
889.2.1 by Martin Packman
Make sure to finish GroupReporter instances even in error cases
2183
            reporter.finish()
1449.2.4 by Curtis Hovey
Break from the look in wait_for_ha so that reporter.finish is always called before pause.
2184
        # XXX sinzui 2014-12-04: bug 1399277 happens because
2185
        # juju claims HA is ready when the monogo replica sets
2186
        # are not. Juju is not fully usable. The replica set
2187
        # lag might be 5 minutes.
2188
        self._backend.pause(300)
703.1.1 by Aaron Bentley
Add ensure-availability stage to industrial_test.
2189
796.2.1 by John George
Add quickstart_deploy.py
2190
    def wait_for_deploy_started(self, service_count=1, timeout=1200):
2191
        """Wait until service_count services are 'started'.
2192
2193
        :param service_count: The number of services for which to wait.
2194
        :param timeout: The number of seconds to wait.
2195
        """
1564.3.9 by Aaron Bentley
Handle soft timeouts in wait_for_deploy_started.
2196
        with self.check_timeouts():
2197
            with self.ignore_soft_deadline():
1738.2.1 by Andrew Beach
Added StatusNotMet exceptions for wait_for_{deploy_started,ha}.
2198
                status = None
1564.3.9 by Aaron Bentley
Handle soft timeouts in wait_for_deploy_started.
2199
                for remaining in until_timeout(timeout):
2200
                    status = self.get_status()
2201
                    if status.get_service_count() >= service_count:
2202
                        return
2203
                else:
1738.2.1 by Andrew Beach
Added StatusNotMet exceptions for wait_for_{deploy_started,ha}.
2204
                    raise ApplicationsNotStarted(self.env.environment, status)
796.2.1 by John George
Add quickstart_deploy.py
2205
1215 by John George
Workload timeout increate to 10 minutes.
2206
    def wait_for_workloads(self, timeout=600, start=None):
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2207
        """Wait until all unit workloads are in a ready state."""
2208
        def status_to_workloads(status):
2209
            unit_states = defaultdict(list)
2210
            for name, unit in status.iter_units():
2211
                workload = unit.get('workload-status')
2212
                if workload is not None:
1196.1.6 by Martin Packman
Changes for review by abentley
2213
                    state = workload['current']
1204.3.1 by Martin Packman
Fix bug introduced with unknown workloads and add test coverage
2214
                else:
2215
                    state = 'unknown'
2216
                unit_states[state].append(name)
2217
            if set(('active', 'unknown')).issuperset(unit_states):
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2218
                return None
1204.3.1 by Martin Packman
Fix bug introduced with unknown workloads and add test coverage
2219
            unit_states.pop('unknown', None)
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2220
            return unit_states
1196.1.6 by Martin Packman
Changes for review by abentley
2221
        reporter = GroupReporter(sys.stdout, 'active')
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2222
        self._wait_for_status(reporter, status_to_workloads, WorkloadsNotReady,
1196.1.4 by Martin Packman
Finish increased sanity on deployer tests
2223
                              timeout=timeout, start=start)
1196.1.1 by Martin Packman
Refactoring jujupy including new wait_for_workloads and iter_units methods
2224
1091.3.1 by James Tunnicliffe
Supporting functions for testing networking between containers in a Juju environment.
2225
    def wait_for(self, thing, search_type, timeout=300):
1091.3.2 by James Tunnicliffe
clean_environment bails early if there is no environment to clean.
2226
        """ Wait for a something (thing) matching none/all/some machines.
2227
2228
        Examples:
2229
          wait_for('containers', 'all')
2230
          This will wait for a container to appear on all machines.
2231
2232
          wait_for('machines-not-0', 'none')
2233
          This will wait for all machines other than 0 to be removed.
2234
2235
        :param thing: string, either 'containers' or 'not-machine-0'
2236
        :param search_type: string containing none, some or all
2237
        :param timeout: number of seconds to wait for condition to be true.
2238
        :return:
2239
        """
2240
        try:
2241
            for status in self.status_until(timeout):
2242
                hit = False
2243
                miss = False
2244
2245
                for machine, details in status.status['machines'].iteritems():
2246
                    if thing == 'containers':
2247
                        if 'containers' in details:
2248
                            hit = True
2249
                        else:
2250
                            miss = True
2251
2252
                    elif thing == 'machines-not-0':
2253
                        if machine != '0':
2254
                            hit = True
2255
                        else:
2256
                            miss = True
2257
2258
                    else:
2259
                        raise ValueError("Unrecognised thing to wait for: %s",
2260
                                         thing)
2261
2262
                if search_type == 'none':
2263
                    if not hit:
2264
                        return
2265
                elif search_type == 'some':
2266
                    if hit:
2267
                        return
2268
                elif search_type == 'all':
2269
                    if not miss:
2270
                        return
2271
        except Exception:
2272
            raise Exception("Timed out waiting for %s" % thing)
1091.3.1 by James Tunnicliffe
Supporting functions for testing networking between containers in a Juju environment.
2273
657.1.4 by Aaron Bentley
Move upgrade functionality to EnvJujuClient.
2274
    def get_matching_agent_version(self, no_build=False):
2275
        # strip the series and srch from the built version.
2276
        version_parts = self.version.split('-')
2277
        if len(version_parts) == 4:
2278
            version_number = '-'.join(version_parts[0:2])
2279
        else:
2280
            version_number = version_parts[0]
2281
        if not no_build and self.env.local:
2282
            version_number += '.1'
2283
        return version_number
2284
1540.3.3 by Christopher Lee
Separate out controller update and model. Only update 'self' model not all models under controller
2285
    def upgrade_juju(self, force_version=True):
2286
        args = ()
2287
        if force_version:
2288
            version = self.get_matching_agent_version(no_build=True)
1612.2.12 by Christopher Lee
Update version argument as per recent change in juju.
2289
            args += ('--agent-version', version)
1540.3.3 by Christopher Lee
Separate out controller update and model. Only update 'self' model not all models under controller
2290
        self._upgrade_juju(args)
2291
2292
    def _upgrade_juju(self, args):
2293
        self.juju('upgrade-juju', args)
2294
1258.2.5 by Curtis Hovey
Added upgrade_mongo to EnvJujuClient.
2295
    def upgrade_mongo(self):
2296
        self.juju('upgrade-mongo', ())
2297
717.2.1 by Aaron Bentley
Extract EnvJujuClient.backup from assess_recovery.
2298
    def backup(self):
808.2.6 by Aaron Bentley
Fix Azure terminate instances to work as Juju expects.
2299
        try:
1419.1.21 by Aaron Bentley
Use get_juju_output for backup.
2300
            output = self.get_juju_output('create-backup')
808.2.6 by Aaron Bentley
Fix Azure terminate instances to work as Juju expects.
2301
        except subprocess.CalledProcessError as e:
1091.4.1 by James Tunnicliffe
Merged upstream
2302
            log.info(e.output)
808.2.6 by Aaron Bentley
Fix Azure terminate instances to work as Juju expects.
2303
            raise
1091.4.1 by James Tunnicliffe
Merged upstream
2304
        log.info(output)
757.1.1 by Curtis Hovey
Support both .tgz and .tar.gz backup file names.
2305
        backup_file_pattern = re.compile('(juju-backup-[0-9-]+\.(t|tar.)gz)')
717.2.1 by Aaron Bentley
Extract EnvJujuClient.backup from assess_recovery.
2306
        match = backup_file_pattern.search(output)
2307
        if match is None:
2308
            raise Exception("The backup file was not found in output: %s" %
2309
                            output)
2310
        backup_file_name = match.group(1)
2311
        backup_file_path = os.path.abspath(backup_file_name)
1091.4.1 by James Tunnicliffe
Merged upstream
2312
        log.info("State-Server backup at %s", backup_file_path)
717.2.1 by Aaron Bentley
Extract EnvJujuClient.backup from assess_recovery.
2313
        return backup_file_path
2314
1221.4.4 by Aaron Bentley
Support create-backup, restore-backup, enable-ha.
2315
    def restore_backup(self, backup_file):
1593.2.5 by Curtis Hovey
Fix tests.
2316
        self.juju(
1593.2.1 by Curtis Hovey
Show the restore output as it happens; do not call status if None was returned.
2317
            'restore-backup',
2318
            ('-b', '--constraints', 'mem=2G', '--file', backup_file))
1221.4.4 by Aaron Bentley
Support create-backup, restore-backup, enable-ha.
2319
2320
    def restore_backup_async(self, backup_file):
1281.1.1 by Aaron Bentley
Rollback r1280 per Ian's email.
2321
        return self.juju_async('restore-backup', ('-b', '--constraints',
2322
                               'mem=2G', '--file', backup_file))
1221.4.4 by Aaron Bentley
Support create-backup, restore-backup, enable-ha.
2323
2324
    def enable_ha(self):
1634.1.1 by Curtis Hovey
enable-ha now takes -c or nothing. not -m.
2325
        self.juju(
1634.1.2 by Curtis Hovey
Added guard for controller name.
2326
            'enable-ha', ('-n', '3', '-c', self.env.controller.name),
1634.1.1 by Curtis Hovey
enable-ha now takes -c or nothing. not -m.
2327
            include_e=False)
1221.4.4 by Aaron Bentley
Support create-backup, restore-backup, enable-ha.
2328
953.3.11 by Nate Finch
set longer timeout for fill log actions, and fix -e testing problem
2329
    def action_fetch(self, id, action=None, timeout="1m"):
953.3.1 by Nate Finch
add log rotation test and support for actions in jujupy
2330
        """Fetches the results of the action with the given id.
2331
953.3.5 by Nate Finch
tests for new action stuff
2332
        Will wait for up to 1 minute for the action results.
953.3.7 by Nate Finch
update for code review comments
2333
        The action name here is just used for an more informational error in
953.3.5 by Nate Finch
tests for new action stuff
2334
        cases where it's available.
953.3.1 by Nate Finch
add log rotation test and support for actions in jujupy
2335
        Returns the yaml output of the fetched action.
2336
        """
1221.1.25 by Aaron Bentley
Use flat names for action commands.
2337
        out = self.get_juju_output("show-action-output", id, "--wait", timeout)
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
2338
        status = yaml.safe_load(out)["status"]
953.3.1 by Nate Finch
add log rotation test and support for actions in jujupy
2339
        if status != "completed":
953.3.8 by Nate Finch
more review changes
2340
            name = ""
953.3.7 by Nate Finch
update for code review comments
2341
            if action is not None:
953.3.8 by Nate Finch
more review changes
2342
                name = " " + action
953.3.9 by Nate Finch
more code review changes
2343
            raise Exception(
2344
                "timed out waiting for action%s to complete during fetch" %
2345
                name)
953.3.2 by Nate Finch
log_rot fixes
2346
        return out
953.3.1 by Nate Finch
add log rotation test and support for actions in jujupy
2347
2348
    def action_do(self, unit, action, *args):
2349
        """Performs the given action on the given unit.
2350
2351
        Action params should be given as args in the form foo=bar.
2352
        Returns the id of the queued action.
2353
        """
953.3.13 by Nate Finch
more code review changes
2354
        args = (unit, action) + args
2355
1221.1.25 by Aaron Bentley
Use flat names for action commands.
2356
        output = self.get_juju_output("run-action", *args)
953.3.9 by Nate Finch
more code review changes
2357
        action_id_pattern = re.compile(
2358
            'Action queued with id: ([a-f0-9\-]{36})')
953.3.1 by Nate Finch
add log rotation test and support for actions in jujupy
2359
        match = action_id_pattern.search(output)
2360
        if match is None:
2361
            raise Exception("Action id not found in output: %s" %
2362
                            output)
2363
        return match.group(1)
2364
953.3.11 by Nate Finch
set longer timeout for fill log actions, and fix -e testing problem
2365
    def action_do_fetch(self, unit, action, timeout="1m", *args):
983.1.1 by Martin Packman
Add new Remote class for accessing juju machines
2366
        """Performs given action on given unit and waits for the results.
953.3.1 by Nate Finch
add log rotation test and support for actions in jujupy
2367
2368
        Action params should be given as args in the form foo=bar.
2369
        Returns the yaml output of the action.
2370
        """
2371
        id = self.action_do(unit, action, *args)
953.3.11 by Nate Finch
set longer timeout for fill log actions, and fix -e testing problem
2372
        return self.action_fetch(id, action, timeout)
953.3.1 by Nate Finch
add log rotation test and support for actions in jujupy
2373
1631.4.9 by Andrew Beach
Adjusted the check in assess_to to use hostname rather than dns name. (Might have to use both.) Updated EnvJujuClient.run to accomidate.
2374
    def run(self, commands, applications=None, machines=None, use_json=True):
2375
        args = []
2376
        if use_json:
2377
            args.extend(['--format', 'json'])
2378
        if applications is not None:
2379
            args.extend(['--application', ','.join(applications)])
2380
        if machines is not None:
2381
            args.extend(['--machine', ','.join(machines)])
2382
        args.extend(commands)
2383
        responces = self.get_juju_output('run', *args)
2384
        if use_json:
2385
            return json.loads(responces)
2386
        else:
2387
            return responces
1449.1.3 by Aaron Bentley
Implement EnvJujuClient.run.
2388
1221.5.1 by Aaron Bentley
Extract EnvJujuClient.list_space.
2389
    def list_space(self):
1221.5.2 by Aaron Bentley
Support list-space for EnvJujuClient.
2390
        return yaml.safe_load(self.get_juju_output('list-space'))
1221.5.1 by Aaron Bentley
Extract EnvJujuClient.list_space.
2391
1221.5.3 by Aaron Bentley
Extract EnvJujuClient.add_space.
2392
    def add_space(self, space):
1221.5.4 by Aaron Bentley
Support space create => add-space.
2393
        self.juju('add-space', (space),)
1221.5.3 by Aaron Bentley
Extract EnvJujuClient.add_space.
2394
1221.5.5 by Aaron Bentley
Extract add_subnet.
2395
    def add_subnet(self, subnet, space):
1221.5.6 by Aaron Bentley
Rename subnet add to add-subnet.
2396
        self.juju('add-subnet', (subnet, space))
1221.5.5 by Aaron Bentley
Extract add_subnet.
2397
1369.2.1 by Seman
Update assess_multi_series_charms to support Juju 1.X tests.
2398
    def is_juju1x(self):
2399
        return self.version.startswith('1.')
2400
1344.3.21 by Nicholas Skaggs
refactored testcase
2401
    def _get_register_command(self, output):
1419.5.4 by Christopher Lee
Fix tests and add another. Change what add_user returns
2402
        """Return register token from add-user output.
2403
2404
        Return the register token supplied within the output from the add-user
2405
        command.
2406
2407
        """
1344.3.19 by Nicholas Skaggs
initial refactoring
2408
        for row in output.split('\n'):
2409
            if 'juju register' in row:
1419.5.4 by Christopher Lee
Fix tests and add another. Change what add_user returns
2410
                command_string = row.strip().lstrip()
2411
                command_parts = command_string.split(' ')
2412
                return command_parts[-1]
1344.3.19 by Nicholas Skaggs
initial refactoring
2413
        raise AssertionError('Juju register command not found in output')
2414
1562.2.3 by Aaron Bentley
Extract add_user, rename existing to add_user_perms.
2415
    def add_user(self, username):
1427.3.17 by Christopher Lee
Revamp user test with chanegs to jujupy too.
2416
        """Adds provided user and return register command arguments.
2417
2418
        :return: Registration token provided by the add-user command.
2419
        """
1562.2.1 by Aaron Bentley
Support new model-less format.
2420
        output = self.get_juju_output(
2421
            'add-user', username, '-c', self.env.controller.name,
2422
            include_e=False)
1562.2.3 by Aaron Bentley
Extract add_user, rename existing to add_user_perms.
2423
        return self._get_register_command(output)
2424
2425
    def add_user_perms(self, username, models=None, permissions='login'):
2426
        """Adds provided user and return register command arguments.
2427
2428
        :return: Registration token provided by the add-user command.
2429
        """
2430
        output = self.add_user(username)
1562.2.1 by Aaron Bentley
Support new model-less format.
2431
        self.grant(username, permissions, models)
1562.2.3 by Aaron Bentley
Extract add_user, rename existing to add_user_perms.
2432
        return output
1427.3.17 by Christopher Lee
Revamp user test with chanegs to jujupy too.
2433
1423.1.1 by Curtis Hovey
Rename 'admin' model to 'controller', preserve old name in EnvJujuClient2B7.
2434
    def revoke(self, username, models=None, permissions='read'):
1344.3.19 by Nicholas Skaggs
initial refactoring
2435
        if models is None:
2436
            models = self.env.environment
1344.3.24 by Nicholas Skaggs
working test_jujupy
2437
1541.2.5 by Curtis Hovey
Moved new functions into EnvJujuClient.
2438
        args = (username, permissions, models)
1344.3.23 by Nicholas Skaggs
Testcase works again
2439
1344.3.28 by Nicholas Skaggs
fixes per Aaron's comments
2440
        self.controller_juju('revoke', args)
1344.3.19 by Nicholas Skaggs
initial refactoring
2441
1466.2.1 by Leo Zhang
Fit version 1.x
2442
    def add_storage(self, unit, storage_type, amount="1"):
2443
        """Add storage instances to service.
2444
2445
        Only type 'disk' is able to add instances.
2446
        """
2447
        self.juju('add-storage', (unit, storage_type + "=" + amount))
2448
2449
    def list_storage(self):
2450
        """Return the storage list."""
2451
        return self.get_juju_output('list-storage', '--format', 'json')
2452
2453
    def list_storage_pool(self):
2454
        """Return the list of storage pool."""
2455
        return self.get_juju_output('list-storage-pools', '--format', 'json')
2456
2457
    def create_storage_pool(self, name, provider, size):
2458
        """Create storage pool."""
2459
        self.juju('create-storage-pool',
2460
                  (name, provider,
2461
                   'size={}'.format(size)))
2462
1477.2.12 by Leo Zhang
Fake merge of trunk
2463
    def disable_user(self, user_name):
2464
        """Disable an user"""
2465
        self.controller_juju('disable-user', (user_name,))
2466
2467
    def enable_user(self, user_name):
2468
        """Enable an user"""
2469
        self.controller_juju('enable-user', (user_name,))
2470
2471
    def logout(self):
2472
        """Logout an user"""
2473
        self.controller_juju('logout', ())
1477.3.1 by Andrew Wilkins
beebop
2474
        self.env.user_name = ''
1477.2.12 by Leo Zhang
Fake merge of trunk
2475
1427.3.18 by Christopher Lee
Further additions re: controller names and usage.
2476
    def register_user(self, user, juju_home, controller_name=None):
1477.2.12 by Leo Zhang
Fake merge of trunk
2477
        """Register `user` for the `client` return the cloned client used."""
2478
        username = user.name
1427.3.18 by Christopher Lee
Further additions re: controller names and usage.
2479
        if controller_name is None:
2480
            controller_name = '{}_controller'.format(username)
1477.2.12 by Leo Zhang
Fake merge of trunk
2481
2482
        model = self.env.environment
1562.2.3 by Aaron Bentley
Extract add_user, rename existing to add_user_perms.
2483
        token = self.add_user_perms(username, models=model,
2484
                                    permissions=user.permissions)
1477.2.12 by Leo Zhang
Fake merge of trunk
2485
        user_client = self.create_cloned_environment(juju_home,
1477.3.1 by Andrew Wilkins
beebop
2486
                                                     controller_name,
2487
                                                     username)
1477.2.12 by Leo Zhang
Fake merge of trunk
2488
2489
        try:
1427.3.17 by Christopher Lee
Revamp user test with chanegs to jujupy too.
2490
            child = user_client.expect('register', (token), include_e=False)
1631.1.1 by Curtis Hovey
Expect password before username when registering a user.
2491
            child.expect('(?i)password')
2492
            child.sendline(username + '_password')
2493
            child.expect('(?i)password')
2494
            child.sendline(username + '_password')
1477.2.12 by Leo Zhang
Fake merge of trunk
2495
            child.expect('(?i)name')
1427.3.18 by Christopher Lee
Further additions re: controller names and usage.
2496
            child.sendline(controller_name)
1477.2.12 by Leo Zhang
Fake merge of trunk
2497
            child.expect(pexpect.EOF)
2498
            if child.isalive():
2499
                raise Exception(
2500
                    'Registering user failed: pexpect session still alive')
2501
        except pexpect.TIMEOUT:
2502
            raise Exception(
2503
                'Registering user failed: pexpect session timed out')
1541.2.8 by Curtis Hovey
Remove Use set-model-config instrad of deploy.
2504
        user_client.env.user_name = username
1477.2.12 by Leo Zhang
Fake merge of trunk
2505
        return user_client
2506
1711.3.4 by Aaron Bentley
Implement register_host.
2507
    def register_host(self, host, email, password):
2508
        child = self.expect('register', ('--no-browser-login', host),
2509
                            include_e=False)
2510
        try:
2511
            child.logfile = sys.stdout
2512
            child.expect('E-Mail:|Enter a name for this controller:')
1721.1.1 by Aaron Bentley
child.match is a regex match, not a string.
2513
            if child.match.group(0) == 'E-Mail:':
1711.3.4 by Aaron Bentley
Implement register_host.
2514
                child.sendline(email)
2515
                child.expect('Password:')
2516
                child.logfile = None
2517
                try:
2518
                    child.sendline(password)
2519
                finally:
2520
                    child.logfile = sys.stdout
2521
                child.expect(r'Two-factor auth \(Enter for none\):')
2522
                child.sendline()
2523
                child.expect('Enter a name for this controller:')
2524
            child.sendline(self.env.controller.name)
2525
            child.expect(pexpect.EOF)
2526
        except pexpect.TIMEOUT:
2527
            raise
2528
            raise Exception(
2529
                'Registering user failed: pexpect session timed out')
2530
1541.2.8 by Curtis Hovey
Remove Use set-model-config instrad of deploy.
2531
    def remove_user(self, username):
2532
        self.juju('remove-user', (username, '-y'), include_e=False)
1541.2.6 by Curtis Hovey
Move new behaviours into EnvJujuClient.
2533
2534
    def create_cloned_environment(
2535
            self, cloned_juju_home, controller_name, user_name=None):
2536
        """Create a cloned environment.
2537
2538
        If `user_name` is passed ensures that the cloned environment is updated
2539
        to match.
2540
2541
        """
2542
        user_client = self.clone(env=self.env.clone())
2543
        user_client.env.juju_home = cloned_juju_home
2544
        if user_name is not None and user_name != self.env.user_name:
2545
            user_client.env.user_name = user_name
2546
            user_client.env.environment = qualified_model_name(
2547
                user_client.env.environment, self.env.user_name)
2548
        # New user names the controller.
2549
        user_client.env.controller = Controller(controller_name)
2550
        return user_client
2551
2552
    def grant(self, user_name, permission, model=None):
2553
        """Grant the user with model or controller permission."""
2554
        if permission in self.controller_permissions:
1427.3.18 by Christopher Lee
Further additions re: controller names and usage.
2555
            self.juju(
2556
                'grant',
2557
                (user_name, permission, '-c', self.env.controller.name),
2558
                include_e=False)
1541.2.6 by Curtis Hovey
Move new behaviours into EnvJujuClient.
2559
        elif permission in self.model_permissions:
2560
            if model is None:
2561
                model = self.model_name
1427.3.17 by Christopher Lee
Revamp user test with chanegs to jujupy too.
2562
            self.juju(
2563
                'grant',
2564
                (user_name, permission, model, '-c', self.env.controller.name),
2565
                include_e=False)
1541.2.6 by Curtis Hovey
Move new behaviours into EnvJujuClient.
2566
        else:
1562.2.2 by Aaron Bentley
Test add_user grants.
2567
            raise ValueError('Unknown permission {}'.format(permission))
1541.2.6 by Curtis Hovey
Move new behaviours into EnvJujuClient.
2568
1556.2.2 by Leo Zhang
changes after review
2569
    def list_clouds(self, format='json'):
2570
        """List all the available clouds."""
2571
        return self.get_juju_output('list-clouds', '--format',
2572
                                    format, include_e=False)
2573
1727.3.3 by Aaron Bentley
Add cloud parameter to add_cloud_interactive.
2574
    def add_cloud_interactive(self, cloud_name, cloud):
1711.6.2 by Aaron Bentley
Test interactive add-cloud against fake juju client.
2575
        child = self.expect('add-cloud', include_e=False)
2576
        try:
2577
            child.logfile = sys.stdout
2578
            child.expect('Select cloud type:')
2579
            child.sendline(cloud['type'])
1727.2.17 by Aaron Bentley
Add bogus type testing.
2580
            child.expect('(Enter a name for the cloud:)|(Select cloud type:)')
2581
            if child.match.group(2) is not None:
2582
                raise TypeNotAccepted('Cloud type not accepted.')
1711.6.2 by Aaron Bentley
Test interactive add-cloud against fake juju client.
2583
            child.sendline(cloud_name)
2584
            if cloud['type'] == 'maas':
2585
                child.expect('Enter the API endpoint url:')
2586
                child.sendline(cloud['endpoint'])
2587
            if cloud['type'] == 'manual':
1727.2.18 by Aaron Bentley
Check invalid name handling.
2588
                child.expect(
2589
                    "(Enter the controller's hostname or IP address:)|"
2590
                    "(Enter a name for the cloud:)")
2591
                if child.match.group(2) is not None:
2592
                    raise NameNotAccepted('Cloud name not accepted.')
1711.6.2 by Aaron Bentley
Test interactive add-cloud against fake juju client.
2593
                child.sendline(cloud['endpoint'])
2594
            if cloud['type'] == 'openstack':
2595
                child.expect('Enter the API endpoint url for the cloud:')
2596
                child.sendline(cloud['endpoint'])
2597
                child.expect(
2598
                    'Select one or more auth types separated by commas:')
2599
                child.sendline(','.join(cloud['auth-types']))
2600
                for num, (name, values) in enumerate(cloud['regions'].items()):
1727.2.5 by Aaron Bentley
Handle invalid auth types
2601
                    child.expect(
2602
                        '(Enter region name:)|(Select one or more auth types'
2603
                        ' separated by commas:)')
2604
                    if child.match.group(2) is not None:
2605
                        raise AuthNotAccepted('Auth was not compatible.')
1711.6.2 by Aaron Bentley
Test interactive add-cloud against fake juju client.
2606
                    child.sendline(name)
2607
                    child.expect('Enter the API endpoint url for the region:')
2608
                    child.sendline(values['endpoint'])
1727.3.2 by Aaron Bentley
Use constant strings where possible.
2609
                    child.expect('Enter another region\? \(Y/n\):')
1711.6.2 by Aaron Bentley
Test interactive add-cloud against fake juju client.
2610
                    if num + 1 < len(cloud['regions']):
2611
                        child.sendline('y')
2612
                    else:
2613
                        child.sendline('n')
2614
            if cloud['type'] == 'vsphere':
2615
                child.expect('Enter the API endpoint url for the cloud:')
2616
                child.sendline(cloud['endpoint'])
2617
                for num, (name, values) in enumerate(cloud['regions'].items()):
2618
                    child.expect('Enter region name:')
2619
                    child.sendline(name)
1727.3.2 by Aaron Bentley
Use constant strings where possible.
2620
                    child.expect('Enter another region\? \(Y/n\):')
1711.6.2 by Aaron Bentley
Test interactive add-cloud against fake juju client.
2621
                    if num + 1 < len(cloud['regions']):
2622
                        child.sendline('y')
2623
                    else:
2624
                        child.sendline('n')
2625
2626
            child.expect(pexpect.EOF)
2627
        except pexpect.TIMEOUT:
2628
            raise Exception(
2629
                'Adding cloud failed: pexpect session timed out')
2630
1556.2.2 by Leo Zhang
changes after review
2631
    def show_controller(self, format='json'):
2632
        """Show controller's status."""
2633
        return self.get_juju_output('show-controller', '--format',
2634
                                    format, include_e=False)
2635
1645 by Andrew Beach
Added show_machine to the client class. Updated and cleaned the hardware functions in assess_constraints.py.
2636
    def show_machine(self, machine):
2637
        """Return data on a machine as a dict."""
2638
        text = self.get_juju_output('show-machine', machine,
2639
                                    '--format', 'yaml')
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
2640
        return yaml.safe_load(text)
1645 by Andrew Beach
Added show_machine to the client class. Updated and cleaned the hardware functions in assess_constraints.py.
2641
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
2642
    def ssh_keys(self, full=False):
2643
        """Give the ssh keys registered for the current model."""
2644
        args = []
2645
        if full:
2646
            args.append('--full')
2647
        return self.get_juju_output('ssh-keys', *args)
2648
2649
    def add_ssh_key(self, *keys):
2650
        """Add one or more ssh keys to the current model."""
2651
        return self.get_juju_output('add-ssh-key', *keys, merge_stderr=True)
2652
2653
    def remove_ssh_key(self, *keys):
2654
        """Remove one or more ssh keys from the current model."""
2655
        return self.get_juju_output('remove-ssh-key', *keys, merge_stderr=True)
2656
2657
    def import_ssh_key(self, *keys):
2658
        """Import ssh keys from one or more identities to the current model."""
2659
        return self.get_juju_output('import-ssh-key', *keys, merge_stderr=True)
2660
1611.1.2 by Andrew Beach
Finished adding list_disabled_commands and the tests for it. assess_block now uses that method instead of calling get_juju_output directly.
2661
    def list_disabled_commands(self):
1611.1.1 by Andrew Beach
Some basic code for the list_disabled_commands and tests for said methods.
2662
        """List all the commands disabled on the model."""
2663
        raw = self.get_juju_output('list-disabled-commands',
2664
                                   '--format', 'yaml')
2665
        return yaml.safe_load(raw)
2666
1723.1.1 by Curtis Hovey
Updated assess_block and jujupy to supprt juju 1x blocks.
2667
    def disable_command(self, command_set, message=''):
1725.1.1 by Curtis Hovey
Rename disable_command_* to command_set_*.
2668
        """Disable a command-set."""
1723.1.1 by Curtis Hovey
Updated assess_block and jujupy to supprt juju 1x blocks.
2669
        return self.juju('disable-command', (command_set, message))
1611.1.3 by Andrew Beach
Added disable/enable_command to EnvJujuClient. Not working yet.
2670
2671
    def enable_command(self, args):
1725.1.1 by Curtis Hovey
Rename disable_command_* to command_set_*.
2672
        """Enable a command-set."""
1611.1.3 by Andrew Beach
Added disable/enable_command to EnvJujuClient. Not working yet.
2673
        return self.juju('enable-command', args)
2674
1614.1.18 by Andrew Beach
Adjusted sync_tools to hide the command line options.
2675
    def sync_tools(self, local_dir=None):
2676
        """Copy tools into a local directory or model."""
2677
        if local_dir is None:
2678
            return self.juju('sync-tools', ())
2679
        else:
2680
            return self.juju('sync-tools', ('--local-dir', local_dir),
2681
                             include_e=False)
1614.1.9 by Andrew Beach
I have been pushing this stuff around all day (and part of yesterday. I got the assess_metadata test working, although currently its results are not very meaningful.
2682
1541.2.6 by Curtis Hovey
Move new behaviours into EnvJujuClient.
2683
1646.1.1 by Christopher Lee
Update order of bootstrap args for 2.0+ (RC clients included, w/ test). Updated tests.
2684
class EnvJujuClientRC(EnvJujuClient):
2685
2686
    def get_bootstrap_args(
2687
            self, upload_tools, config_filename, bootstrap_series=None,
2688
            credential=None, auto_upgrade=False, metadata_source=None,
1670.1.1 by Martin Packman
Support --no-gui flag to juju bootstrap
2689
            to=None, no_gui=False, agent_version=None):
1646.1.1 by Christopher Lee
Update order of bootstrap args for 2.0+ (RC clients included, w/ test). Updated tests.
2690
        """Return the bootstrap arguments for the substrate."""
2691
        if self.env.joyent:
2692
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
2693
            constraints = 'mem=2G cpu-cores=1'
2694
        else:
2695
            constraints = 'mem=2G'
2696
        cloud_region = self.get_cloud_region(self.env.get_cloud(),
2697
                                             self.env.get_region())
2698
        # Note controller name before cloud_region
2699
        args = ['--constraints', constraints,
2700
                self.env.environment,
2701
                cloud_region,
2702
                '--config', config_filename,
2703
                '--default-model', self.env.environment]
2704
        if upload_tools:
2705
            if agent_version is not None:
2706
                raise ValueError(
2707
                    'agent-version may not be given with upload-tools.')
2708
            args.insert(0, '--upload-tools')
2709
        else:
2710
            if agent_version is None:
2711
                agent_version = self.get_matching_agent_version()
2712
            args.extend(['--agent-version', agent_version])
2713
        if bootstrap_series is not None:
2714
            args.extend(['--bootstrap-series', bootstrap_series])
2715
        if credential is not None:
2716
            args.extend(['--credential', credential])
2717
        if metadata_source is not None:
2718
            args.extend(['--metadata-source', metadata_source])
2719
        if auto_upgrade:
2720
            args.append('--auto-upgrade')
2721
        if to is not None:
2722
            args.extend(['--to', to])
1670.1.1 by Martin Packman
Support --no-gui flag to juju bootstrap
2723
        if no_gui:
2724
            args.append('--no-gui')
1646.1.1 by Christopher Lee
Update order of bootstrap args for 2.0+ (RC clients included, w/ test). Updated tests.
2725
        return tuple(args)
2726
2727
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
2728
class EnvJujuClient1X(EnvJujuClientRC):
1662.1.5 by Andrew Beach
Removed EnvJujuClient.
2729
    """Base for all 1.x client drivers."""
1258.1.3 by Aaron Bentley
Support transitional jujus.
2730
1662.1.5 by Andrew Beach
Removed EnvJujuClient.
2731
    default_backend = Juju1XBackend
1363.7.1 by Aaron Bentley
Implement _shell_environ on backend.
2732
1465.2.4 by Aaron Bentley
Create a JujuData directly instead of from SimpleEnvironment.
2733
    config_class = SimpleEnvironment
2734
1692.1.1 by Andrew Beach
ServiceStatus->Status1X
2735
    status_class = Status1X
1662.1.13 by Andrew Beach
Removed EnvJujuClient2B8 and its tests.
2736
1662.1.4 by Andrew Beach
Deleted EnvJujuClient2A1 and removed support for testing with it.
2737
    # The environments.yaml options that are replaced by bootstrap options.
2738
    # For Juju 1.x, no bootstrap options are used.
2739
    bootstrap_replaces = frozenset()
2740
2741
    destroy_model_command = 'destroy-environment'
2742
2743
    supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE])
2744
2745
    agent_metadata_url = 'tools-metadata-url'
1221.1.17 by Aaron Bentley
Provide for a separate 2.0-alpha1 client.
2746
1221.5.15 by Aaron Bentley
Use 'show-status' instead of 'status'.
2747
    _show_status = 'status'
2748
1725.1.1 by Curtis Hovey
Rename disable_command_* to command_set_*.
2749
    command_set_destroy_model = 'destroy-environment'
2750
2751
    command_set_all = 'all-changes'
1723.1.1 by Curtis Hovey
Updated assess_block and jujupy to supprt juju 1x blocks.
2752
1662.1.5 by Andrew Beach
Removed EnvJujuClient.
2753
    @classmethod
2754
    def _get_env(cls, env):
2755
        if isinstance(env, JujuData):
2756
            raise IncompatibleConfigClass(
2757
                'JujuData cannot be used with {}'.format(cls.__name__))
2758
        return env
1363.7.1 by Aaron Bentley
Implement _shell_environ on backend.
2759
1240.2.1 by Aaron Bentley
Support models/cache.yaml
2760
    def get_cache_path(self):
2761
        return get_cache_path(self.env.juju_home, models=False)
2762
1221.4.2 by Aaron Bentley
Introduce EnvJujuClient.remove_service.
2763
    def remove_service(self, service):
2764
        self.juju('destroy-service', (service,))
2765
1221.4.4 by Aaron Bentley
Support create-backup, restore-backup, enable-ha.
2766
    def backup(self):
2767
        environ = self._shell_environ()
2768
        # juju-backup does not support the -e flag.
2769
        environ['JUJU_ENV'] = self.env.environment
2770
        try:
2771
            # Mutate os.environ instead of supplying env parameter so Windows
2772
            # can search env['PATH']
2773
            with scoped_environ(environ):
1326.1.2 by Aaron Bentley
Emit backup command args.
2774
                args = ['juju', 'backup']
2775
                log.info(' '.join(args))
2776
                output = subprocess.check_output(args)
1221.4.4 by Aaron Bentley
Support create-backup, restore-backup, enable-ha.
2777
        except subprocess.CalledProcessError as e:
2778
            log.info(e.output)
2779
            raise
2780
        log.info(output)
2781
        backup_file_pattern = re.compile('(juju-backup-[0-9-]+\.(t|tar.)gz)')
2782
        match = backup_file_pattern.search(output)
2783
        if match is None:
2784
            raise Exception("The backup file was not found in output: %s" %
2785
                            output)
2786
        backup_file_name = match.group(1)
2787
        backup_file_path = os.path.abspath(backup_file_name)
2788
        log.info("State-Server backup at %s", backup_file_path)
2789
        return backup_file_path
2790
2791
    def restore_backup(self, backup_file):
2792
        return self.get_juju_output('restore', '--constraints', 'mem=2G',
2793
                                    backup_file)
2794
2795
    def restore_backup_async(self, backup_file):
2796
        return self.juju_async('restore', ('--constraints', 'mem=2G',
2797
                                           backup_file))
2798
2799
    def enable_ha(self):
2800
        self.juju('ensure-availability', ('-n', '3'))
2801
1306.1.23 by Curtis Hovey
Added 1.x variations to list-controllers and list-models.
2802
    def list_models(self):
2803
        """List the models registered with the current controller."""
2804
        log.info('The model is environment {}'.format(self.env.environment))
2805
1556.2.3 by Leo Zhang
changes after review
2806
    def list_clouds(self, format='json'):
2807
        """List all the available clouds."""
2808
        return {}
2809
2810
    def show_controller(self, format='json'):
2811
        """Show controller's status."""
2812
        return {}
2813
1306.1.23 by Curtis Hovey
Added 1.x variations to list-controllers and list-models.
2814
    def get_models(self):
2815
        """return a models dict with a 'models': [] key-value pair."""
2816
        return {}
2817
2818
    def list_controllers(self):
2819
        """List the controllers."""
2820
        log.info(
2821
            'The controller is environment {}'.format(self.env.environment))
2822
1259.1.2 by Aaron Bentley
Implement get_controller_member_status.
2823
    @staticmethod
2824
    def get_controller_member_status(info_dict):
2825
        return info_dict.get('state-server-member-status')
1259.1.1 by Aaron Bentley
Expect controller-member-status in 2.0-alpha2
2826
1221.1.25 by Aaron Bentley
Use flat names for action commands.
2827
    def action_fetch(self, id, action=None, timeout="1m"):
2828
        """Fetches the results of the action with the given id.
2829
2830
        Will wait for up to 1 minute for the action results.
2831
        The action name here is just used for an more informational error in
2832
        cases where it's available.
2833
        Returns the yaml output of the fetched action.
2834
        """
2835
        # the command has to be "action fetch" so that the -e <env> args are
2836
        # placed after "fetch", since that's where action requires them to be.
2837
        out = self.get_juju_output("action fetch", id, "--wait", timeout)
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
2838
        status = yaml.safe_load(out)["status"]
1221.1.25 by Aaron Bentley
Use flat names for action commands.
2839
        if status != "completed":
2840
            name = ""
2841
            if action is not None:
2842
                name = " " + action
2843
            raise Exception(
2844
                "timed out waiting for action%s to complete during fetch" %
2845
                name)
2846
        return out
2847
2848
    def action_do(self, unit, action, *args):
2849
        """Performs the given action on the given unit.
2850
2851
        Action params should be given as args in the form foo=bar.
2852
        Returns the id of the queued action.
2853
        """
2854
        args = (unit, action) + args
2855
2856
        # the command has to be "action do" so that the -e <env> args are
2857
        # placed after "do", since that's where action requires them to be.
2858
        output = self.get_juju_output("action do", *args)
2859
        action_id_pattern = re.compile(
2860
            'Action queued with id: ([a-f0-9\-]{36})')
2861
        match = action_id_pattern.search(output)
2862
        if match is None:
2863
            raise Exception("Action id not found in output: %s" %
2864
                            output)
2865
        return match.group(1)
2866
1662.1.13 by Andrew Beach
Removed EnvJujuClient2B8 and its tests.
2867
    def run(self, commands, applications):
2868
        responses = self.get_juju_output(
2869
            'run', '--format', 'json', '--service', ','.join(applications),
2870
            *commands)
2871
        return json.loads(responses)
2872
1221.5.2 by Aaron Bentley
Support list-space for EnvJujuClient.
2873
    def list_space(self):
2874
        return yaml.safe_load(self.get_juju_output('space list'))
2875
1221.5.4 by Aaron Bentley
Support space create => add-space.
2876
    def add_space(self, space):
2877
        self.juju('space create', (space),)
2878
1221.5.6 by Aaron Bentley
Rename subnet add to add-subnet.
2879
    def add_subnet(self, subnet, space):
2880
        self.juju('subnet add', (subnet, space))
2881
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
2882
    def add_user_perms(self, username, models=None, permissions='read'):
1662.1.16 by Andrew Beach
Used JESNotSupported to remove unsupported functionality from EnvJujuClient1X.
2883
        raise JESNotSupported()
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
2884
2885
    def grant(self, user_name, permission, model=None):
1662.1.16 by Andrew Beach
Used JESNotSupported to remove unsupported functionality from EnvJujuClient1X.
2886
        raise JESNotSupported()
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
2887
2888
    def revoke(self, username, models=None, permissions='read'):
1662.1.16 by Andrew Beach
Used JESNotSupported to remove unsupported functionality from EnvJujuClient1X.
2889
        raise JESNotSupported()
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
2890
1251.1.1 by Aaron Bentley
Implement and use set_model_constraints.
2891
    def set_model_constraints(self, constraints):
2892
        constraint_strings = self._dict_as_option_strings(constraints)
2893
        return self.juju('set-constraints', constraint_strings)
2894
1239.1.1 by Aaron Bentley
Implement and use EnvJujuClient.set_config.
2895
    def set_config(self, service, options):
2896
        option_strings = ['{}={}'.format(*item) for item in options.items()]
2897
        self.juju('set', (service,) + tuple(option_strings))
2898
1221.5.12 by Aaron Bentley
Support get-config.
2899
    def get_config(self, service):
1646.2.1 by Andrew Beach
Removed yaml_loads and replaced all calls with a direct use of yaml.safe_load.
2900
        return yaml.safe_load(self.get_juju_output('get', service))
1221.5.12 by Aaron Bentley
Support get-config.
2901
1243.1.1 by Aaron Bentley
Support rename of get-env/set-env => get-model-config/set-model-config.
2902
    def get_model_config(self):
2903
        """Return the value of the environment's configured option."""
2904
        return yaml.safe_load(self.get_juju_output('get-env'))
2905
2906
    def get_env_option(self, option):
2907
        """Return the value of the environment's configured option."""
2908
        return self.get_juju_output('get-env', option)
2909
2910
    def set_env_option(self, option, value):
2911
        """Set the value of the option in the environment."""
2912
        option_value = "%s=%s" % (option, value)
2913
        return self.juju('set-env', (option_value,))
2914
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
2915
    def unset_env_option(self, option):
2916
        """Unset the value of the option in the environment."""
1662.1.16 by Andrew Beach
Used JESNotSupported to remove unsupported functionality from EnvJujuClient1X.
2917
        return self.juju('set-env', ('{}='.format(option),))
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
2918
1493.1.1 by Martin
Rename methods and variables refering to admin model to new term controller model
2919
    def _cmd_model(self, include_e, controller):
2920
        if controller:
2921
            return self.get_controller_model_name()
1427.1.4 by Christopher Lee
Fix failing tests, minor tweak to handle 1.x clients.
2922
        elif self.env is None or not include_e:
2923
            return None
2924
        else:
1477.3.1 by Andrew Wilkins
beebop
2925
            return unqualified_model_name(self.model_name)
1427.1.4 by Christopher Lee
Fix failing tests, minor tweak to handle 1.x clients.
2926
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
2927
    def update_user_name(self):
2928
        return
2929
1657.1.1 by Martin Packman
Work in progress network safe endpoint binding test
2930
    def _get_substrate_constraints(self):
2931
        if self.env.joyent:
2932
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
2933
            return 'mem=2G cpu-cores=1'
2934
        else:
2935
            return 'mem=2G'
2936
1579.1.6 by Andrew Beach
Added credential argument to EnvJujuClient.bootstrap and EnvJujuClient.get_bootstrap_args.
2937
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2938
                           credential=None):
1251.2.7 by Aaron Bentley
Update docs
2939
        """Return the bootstrap arguments for the substrate."""
1579.1.6 by Andrew Beach
Added credential argument to EnvJujuClient.bootstrap and EnvJujuClient.get_bootstrap_args.
2940
        if credential is not None:
2941
            raise ValueError(
2942
                '--credential is not supported by this juju version.')
1260.2.30 by Aaron Bentley
Updates from review
2943
        constraints = self._get_substrate_constraints()
1242.3.1 by Aaron Bentley
Update get_bootstrap_args.
2944
        args = ('--constraints', constraints)
2945
        if upload_tools:
2946
            args = ('--upload-tools',) + args
2947
        if bootstrap_series is not None:
1699.1.1 by Aaron Bentley
Implement and use SimpleEnvironment and JujuData get_option.
2948
            env_val = self.env.get_option('default-series')
1242.3.1 by Aaron Bentley
Update get_bootstrap_args.
2949
            if bootstrap_series != env_val:
2950
                raise BootstrapMismatch(
2951
                    'bootstrap-series', bootstrap_series, 'default-series',
2952
                    env_val)
2953
        return args
2954
1662.1.5 by Andrew Beach
Removed EnvJujuClient.
2955
    def bootstrap(self, upload_tools=False, bootstrap_series=None):
2956
        """Bootstrap a controller."""
2957
        self._check_bootstrap()
2958
        args = self.get_bootstrap_args(upload_tools, bootstrap_series)
2959
        self.juju('bootstrap', args, self.env.needs_sudo())
2960
2961
    @contextmanager
2962
    def bootstrap_async(self, upload_tools=False):
2963
        self._check_bootstrap()
2964
        args = self.get_bootstrap_args(upload_tools)
2965
        with self.juju_async('bootstrap', args):
2966
            yield
2967
            log.info('Waiting for bootstrap of {}.'.format(
2968
                self.env.environment))
2969
1242.2.1 by Aaron Bentley
EnvJujuClient assumes JES enabled, since it assumes 2.0.
2970
    def get_jes_command(self):
1662.1.1 by Andrew Beach
Cut out EnvJujuClient26. Will have to split it into smaller pieces for merging.
2971
        raise JESNotSupported()
2972
2973
    def enable_jes(self):
1242.2.1 by Aaron Bentley
EnvJujuClient assumes JES enabled, since it assumes 2.0.
2974
        raise JESNotSupported()
2975
1540.3.3 by Christopher Lee
Separate out controller update and model. Only update 'self' model not all models under controller
2976
    def upgrade_juju(self, force_version=True):
2977
        args = ()
2978
        if force_version:
2979
            version = self.get_matching_agent_version(no_build=True)
2980
            args += ('--version', version)
2981
        if self.env.local:
2982
            args += ('--upload-tools',)
2983
        self._upgrade_juju(args)
1540.3.1 by Christopher Lee
Update upgrade methods to work with juju 2.0. Upgrades controller first then any other hosted models.
2984
1363.4.3 by Aaron Bentley
Provide config, rather than a client, to create_model.
2985
    def make_model_config(self):
2986
        config_dict = make_safe_config(self)
2987
        # Strip unneeded variables.
2988
        return config_dict
2989
1363.4.4 by Aaron Bentley
Rename create_model to add_model.
2990
    def _add_model(self, model_name, config_file):
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
2991
        seen_cmd = self.get_jes_command()
1242.2.1 by Aaron Bentley
EnvJujuClient assumes JES enabled, since it assumes 2.0.
2992
        if seen_cmd == SYSTEM:
1363.4.3 by Aaron Bentley
Provide config, rather than a client, to create_model.
2993
            controller_option = ('-s', self.env.environment)
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
2994
        else:
1363.4.3 by Aaron Bentley
Provide config, rather than a client, to create_model.
2995
            controller_option = ('-c', self.env.environment)
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
2996
        self.juju(_jes_cmds[seen_cmd]['create'], controller_option + (
1363.4.3 by Aaron Bentley
Provide config, rather than a client, to create_model.
2997
            model_name, '--config', config_file), include_e=False)
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
2998
1221.1.15 by Aaron Bentley
Implement and use destroy_model for destroying models.
2999
    def destroy_model(self):
3000
        """With JES enabled, destroy-environment destroys the model."""
1685.1.1 by Andrew Beach
Started unplugging kill-controller from _jes_cmds. That part is actually done, getting the new part to work is trickier.
3001
        return self.destroy_environment(force=False)
3002
3003
    def kill_controller(self):
1688.1.16 by Andrew Beach
Updated doc-strings.
3004
        """Destroy the environment, with force. Hard kill option.
3005
3006
        :return: Subprocess's exit code."""
1685.1.5 by Andrew Beach
Updated kill/destroy_controller.
3007
        return self.juju(
3008
            'destroy-environment', (self.env.environment, '--force', '-y'),
3009
            check=False, include_e=False, timeout=get_teardown_timeout(self))
1221.1.15 by Aaron Bentley
Implement and use destroy_model for destroying models.
3010
1688.1.15 by Andrew Beach
Added --destroy-all-models flag to destroy-controller.
3011
    def destroy_controller(self, all_models=False):
1688.1.16 by Andrew Beach
Updated doc-strings.
3012
        """Destroy the environment, with force. Soft kill option.
3013
3014
        :param all_models: Ignored.
3015
        :raises: subprocess.CalledProcessError if the operation fails."""
1685.1.5 by Andrew Beach
Updated kill/destroy_controller.
3016
        return self.juju(
3017
            'destroy-environment', (self.env.environment, '-y'),
3018
            include_e=False, timeout=get_teardown_timeout(self))
1685.1.4 by Andrew Beach
Added the soft kill destroy-controller.
3019
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
3020
    def destroy_environment(self, force=True, delete_jenv=False):
3021
        if force:
3022
            force_arg = ('--force',)
3023
        else:
3024
            force_arg = ()
3025
        exit_status = self.juju(
3026
            'destroy-environment',
3027
            (self.env.environment,) + force_arg + ('-y',),
1685.1.1 by Andrew Beach
Started unplugging kill-controller from _jes_cmds. That part is actually done, getting the new part to work is trickier.
3028
            check=False, include_e=False,
1535.1.4 by Curtis Hovey
Extracted get_teardown_timeout.
3029
            timeout=get_teardown_timeout(self))
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
3030
        if delete_jenv:
3031
            jenv_path = get_jenv_path(self.env.juju_home, self.env.environment)
3032
            ensure_deleted(jenv_path)
3033
        return exit_status
3034
1315.2.8 by Aaron Bentley
Dump logs for all models, including on JES.
3035
    def _get_models(self):
3036
        """return a list of model dicts."""
1452.1.1 by Curtis Hovey
Ensure _get_models() is compatible with 1.x
3037
        try:
3038
            return yaml.safe_load(self.get_juju_output(
3039
                'environments', '-s', self.env.environment, '--format', 'yaml',
3040
                include_e=False))
3041
        except subprocess.CalledProcessError:
3042
            # This *private* method attempts to use a 1.25 JES feature.
3043
            # The JES design is dead. The private method is not used to
3044
            # directly test juju cli; the failure is not a contract violation.
3045
            log.info('Call to JES juju environments failed, falling back.')
3046
            return []
1315.2.8 by Aaron Bentley
Dump logs for all models, including on JES.
3047
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
3048
    def get_model_uuid(self):
1662.1.16 by Andrew Beach
Used JESNotSupported to remove unsupported functionality from EnvJujuClient1X.
3049
        raise JESNotSupported()
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
3050
1259.2.1 by Martin Packman
Add flag to run_deployer to use juju 2.0 bundle deployment
3051
    def deploy_bundle(self, bundle, timeout=_DEFAULT_BUNDLE_TIMEOUT):
3052
        """Deploy bundle using deployer for Juju 1.X version."""
3053
        self.deployer(bundle, timeout=timeout)
3054
1341.2.10 by Aaron Bentley
Fix deployer implementation.
3055
    def deployer(self, bundle_template, name=None, deploy_delay=10,
3056
                 timeout=3600):
1460.1.2 by Aaron Bentley
Updates from review
3057
        """Deploy a bundle using deployer."""
1341.2.10 by Aaron Bentley
Fix deployer implementation.
3058
        bundle = self.format_bundle(bundle_template)
1363.1.1 by Seman
Updated Deployer to support Juju 2.X.
3059
        args = (
3060
            '--debug',
3061
            '--deploy-delay', str(deploy_delay),
3062
            '--timeout', str(timeout),
3063
            '--config', bundle,
3064
        )
3065
        if name:
3066
            args += (name,)
3067
        self.juju('deployer', args, self.env.needs_sudo())
3068
1662.1.5 by Andrew Beach
Removed EnvJujuClient.
3069
    def deploy(self, charm, repository=None, to=None, series=None,
3070
               service=None, force=False, storage=None, constraints=None):
3071
        args = [charm]
3072
        if repository is not None:
3073
            args.extend(['--repository', repository])
3074
        if to is not None:
3075
            args.extend(['--to', to])
3076
        if service is not None:
3077
            args.extend([service])
3078
        if storage is not None:
3079
            args.extend(['--storage', storage])
3080
        if constraints is not None:
3081
            args.extend(['--constraints', constraints])
3082
        return self.juju('deploy', tuple(args))
3083
1369.1.1 by Aaron Bentley
Implement upgrade_charm, switch industrial_test to it.
3084
    def upgrade_charm(self, service, charm_path=None):
3085
        args = (service,)
3086
        if charm_path is not None:
3087
            repository = os.path.dirname(os.path.dirname(charm_path))
3088
            args = args + ('--repository', repository)
3089
        self.juju('upgrade-charm', args)
3090
1662.1.7 by Andrew Beach
Removed EnvJujuClient2B2.
3091
    def get_controller_client(self):
3092
        """Return a client for the controller model.  May return self."""
3093
        return self
3094
3095
    def get_controller_model_name(self):
3096
        """Return the name of the 'controller' model.
3097
1662.1.9 by Andrew Beach
Removed the JES code from EnvJujuClient1X.get_controller_model_name.
3098
        Return the name of the 1.x environment."""
1662.1.7 by Andrew Beach
Removed EnvJujuClient2B2.
3099
        return self.env.environment
3100
1306.1.5 by Curtis Hovey
Added get_controller_endpoint for get_endpoints.
3101
    def get_controller_endpoint(self):
3102
        """Return the address of the state-server leader."""
1306.1.19 by Curtis Hovey
Fix calls to show-controller with include_e=False.
3103
        endpoint = self.get_juju_output('api-endpoints')
1310.1.2 by Curtis Hovey
Address ipv6 split address and port.
3104
        address, port = split_address_port(endpoint)
1306.1.5 by Curtis Hovey
Added get_controller_endpoint for get_endpoints.
3105
        return address
3106
1258.2.5 by Curtis Hovey
Added upgrade_mongo to EnvJujuClient.
3107
    def upgrade_mongo(self):
3108
        raise UpgradeMongoNotSupported()
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
3109
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
3110
    def create_cloned_environment(
3111
            self, cloned_juju_home, controller_name, user_name=None):
3112
        """Create a cloned environment.
3113
3114
        `user_name` is unused in this version of juju.
3115
        """
3116
        user_client = self.clone(env=self.env.clone())
3117
        user_client.env.juju_home = cloned_juju_home
3118
        # New user names the controller.
3119
        user_client.env.controller = Controller(controller_name)
3120
        return user_client
3121
1466.2.1 by Leo Zhang
Fit version 1.x
3122
    def add_storage(self, unit, storage_type, amount="1"):
3123
        """Add storage instances to service.
3124
3125
        Only type 'disk' is able to add instances.
3126
        """
3127
        self.juju('storage add', (unit, storage_type + "=" + amount))
3128
3129
    def list_storage(self):
3130
        """Return the storage list."""
3131
        return self.get_juju_output('storage list', '--format', 'json')
3132
3133
    def list_storage_pool(self):
3134
        """Return the list of storage pool."""
3135
        return self.get_juju_output('storage pool list', '--format', 'json')
3136
3137
    def create_storage_pool(self, name, provider, size):
3138
        """Create storage pool."""
3139
        self.juju('storage pool create',
3140
                  (name, provider,
3141
                   'size={}'.format(size)))
3142
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
3143
    def ssh_keys(self, full=False):
3144
        """Give the ssh keys registered for the current model."""
3145
        args = []
3146
        if full:
3147
            args.append('--full')
1581.1.1 by Martin Packman
Make assess_ssh_keys 1.25 compatible and update for landed fix
3148
        return self.get_juju_output('authorized-keys list', *args)
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
3149
3150
    def add_ssh_key(self, *keys):
3151
        """Add one or more ssh keys to the current model."""
1581.1.1 by Martin Packman
Make assess_ssh_keys 1.25 compatible and update for landed fix
3152
        return self.get_juju_output('authorized-keys add', *keys,
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
3153
                                    merge_stderr=True)
3154
3155
    def remove_ssh_key(self, *keys):
3156
        """Remove one or more ssh keys from the current model."""
1581.1.1 by Martin Packman
Make assess_ssh_keys 1.25 compatible and update for landed fix
3157
        return self.get_juju_output('authorized-keys delete', *keys,
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
3158
                                    merge_stderr=True)
3159
3160
    def import_ssh_key(self, *keys):
3161
        """Import ssh keys from one or more identities to the current model."""
1581.1.1 by Martin Packman
Make assess_ssh_keys 1.25 compatible and update for landed fix
3162
        return self.get_juju_output('authorized-keys import', *keys,
1553.1.1 by Martin Packman
Add support for ssh key related commands to jujupy
3163
                                    merge_stderr=True)
3164
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
3165
    def list_disabled_commands(self):
3166
        """List all the commands disabled on the model."""
3167
        raw = self.get_juju_output('block list', '--format', 'yaml')
3168
        return yaml.safe_load(raw)
3169
1723.1.1 by Curtis Hovey
Updated assess_block and jujupy to supprt juju 1x blocks.
3170
    def disable_command(self, command_set, message=''):
1725.1.1 by Curtis Hovey
Rename disable_command_* to command_set_*.
3171
        """Disable a command-set."""
1723.1.1 by Curtis Hovey
Updated assess_block and jujupy to supprt juju 1x blocks.
3172
        return self.juju('block {}'.format(command_set), (message, ))
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
3173
3174
    def enable_command(self, args):
1725.1.1 by Curtis Hovey
Rename disable_command_* to command_set_*.
3175
        """Enable a command-set."""
1662.1.14 by Andrew Beach
Removed EnvJujuClient2B9.
3176
        return self.juju('unblock', args)
3177
1221.1.1 by Aaron Bentley
Support -model renames in jujupy and test_jujupy.
3178
3179
class EnvJujuClient22(EnvJujuClient1X):
953.3.9 by Nate Finch
more code review changes
3180
1263.1.1 by Martin Packman
Refactor how juju feature flags are handled with new enable_feature method
3181
    used_feature_flags = frozenset(['actions'])
953.3.9 by Nate Finch
more code review changes
3182
1263.1.1 by Martin Packman
Refactor how juju feature flags are handled with new enable_feature method
3183
    def __init__(self, *args, **kwargs):
3184
        super(EnvJujuClient22, self).__init__(*args, **kwargs)
3185
        self.feature_flags.add('actions')
953.3.9 by Nate Finch
more code review changes
3186
3187
1662.1.1 by Andrew Beach
Cut out EnvJujuClient26. Will have to split it into smaller pieces for merging.
3188
class EnvJujuClient25(EnvJujuClient1X):
3189
    """Drives Juju 2.5-series clients."""
3190
3191
    used_feature_flags = frozenset()
1221.1.8 by Aaron Bentley
Implement EnvJujuClient.clone.
3192
1162.2.20 by Aaron Bentley
Implement disable_jes.
3193
    def disable_jes(self):
1662.1.1 by Andrew Beach
Cut out EnvJujuClient26. Will have to split it into smaller pieces for merging.
3194
        self.feature_flags.discard('jes')
1074.2.1 by Aaron Bentley
Add EnvJujuClient support for Juju 2.6.
3195
3196
957.2.1 by Aaron Bentley
Treat cloudsigma as provisional in 2.5
3197
class EnvJujuClient24(EnvJujuClient25):
1662.1.1 by Andrew Beach
Cut out EnvJujuClient26. Will have to split it into smaller pieces for merging.
3198
    """Similar to EnvJujuClient25."""
1044.1.9 by Aaron Bentley
Update and add tests.
3199
1256.1.2 by Aaron Bentley
Expect old add-machine behaviour from 1.24.x and earlier.
3200
    def add_ssh_machines(self, machines):
3201
        for machine in machines:
3202
            self.juju('add-machine', ('ssh:' + machine,))
3203
957.2.1 by Aaron Bentley
Treat cloudsigma as provisional in 2.5
3204
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
3205
def get_local_root(juju_home, env):
3206
    return os.path.join(juju_home, env.environment)
3207
3208
663.1.8 by Aaron Bentley
Extract temp_bootstrap_env from bootstrap_from_env.
3209
def bootstrap_from_env(juju_home, client):
3210
    with temp_bootstrap_env(juju_home, client):
3211
        client.bootstrap()
3212
3213
796.2.4 by John George
Added support for setting the juju path, series and agent_url.
3214
def quickstart_from_env(juju_home, client, bundle):
3215
    with temp_bootstrap_env(juju_home, client):
3216
        client.quickstart(bundle)
3217
3218
696.1.10 by Aaron Bentley
Move uniquify_local to jujupy.
3219
def uniquify_local(env):
3220
    """Ensure that local environments have unique port settings.
3221
3222
    This allows local environments to be duplicated despite
3223
    https://bugs.launchpad.net/bugs/1382131
3224
    """
3225
    if not env.local:
3226
        return
3227
    port_defaults = {
3228
        'api-port': 17070,
3229
        'state-port': 37017,
3230
        'storage-port': 8040,
3231
        'syslog-port': 6514,
3232
    }
1674.1.16 by Aaron Bentley
Switch call sites to update_config.
3233
    new_config = {}
696.1.10 by Aaron Bentley
Move uniquify_local to jujupy.
3234
    for key, default in port_defaults.items():
1699.1.2 by Aaron Bentley
Convert direct config access to get_option.
3235
        new_config[key] = env.get_option(key, default) + 1
1674.1.16 by Aaron Bentley
Switch call sites to update_config.
3236
    env.update_config(new_config)
696.1.10 by Aaron Bentley
Move uniquify_local to jujupy.
3237
3238
1044.1.9 by Aaron Bentley
Update and add tests.
3239
def dump_environments_yaml(juju_home, config):
1600.1.1 by Andrew Beach
Added docstrings to jujupy.py.
3240
    """Dump yaml data to the environment file.
3241
3242
    :param juju_home: Path to the JUJU_HOME directory.
3243
    :param config: Dictionary repersenting yaml data to dump."""
1044.1.9 by Aaron Bentley
Update and add tests.
3244
    environments_path = get_environments_path(juju_home)
3245
    with open(environments_path, 'w') as config_file:
3246
        yaml.safe_dump(config, config_file)
3247
3248
663.1.8 by Aaron Bentley
Extract temp_bootstrap_env from bootstrap_from_env.
3249
@contextmanager
807.1.1 by Aaron Bentley
Avoid creating temp juju homes inside temp juju homes.
3250
def _temp_env(new_config, parent=None, set_home=True):
716.1.2 by Aaron Bentley
Extract _temp_env from temp_bootstrap_env.
3251
    """Use the supplied config as juju environment.
3252
3253
    This is not a fully-formed version for bootstrapping.  See
3254
    temp_bootstrap_env.
3255
    """
3256
    with temp_dir(parent) as temp_juju_home:
1044.1.9 by Aaron Bentley
Update and add tests.
3257
        dump_environments_yaml(temp_juju_home, new_config)
807.1.1 by Aaron Bentley
Avoid creating temp juju homes inside temp juju homes.
3258
        if set_home:
1695.2.2 by Andrew Beach
Bits of clean-up that happened while I was working on finding public-clouds.yaml.
3259
            with scoped_environ():
807.1.1 by Aaron Bentley
Avoid creating temp juju homes inside temp juju homes.
3260
                os.environ['JUJU_HOME'] = temp_juju_home
1258.1.1 by Aaron Bentley
Use JUJU_DATA where client may be 2.0alpha2+
3261
                os.environ['JUJU_DATA'] = temp_juju_home
1695.2.2 by Andrew Beach
Bits of clean-up that happened while I was working on finding public-clouds.yaml.
3262
                yield temp_juju_home
3263
        else:
716.1.2 by Aaron Bentley
Extract _temp_env from temp_bootstrap_env.
3264
            yield temp_juju_home
3265
3266
1044.1.9 by Aaron Bentley
Update and add tests.
3267
def jes_home_path(juju_home, dir_name):
1048.1.3 by Aaron Bentley
Add make_jes_home and jes_home_path to tests.
3268
    return os.path.join(juju_home, 'jes-homes', dir_name)
1044.1.9 by Aaron Bentley
Update and add tests.
3269
3270
1240.2.1 by Aaron Bentley
Support models/cache.yaml
3271
def get_cache_path(juju_home, models=False):
3272
    if models:
3273
        root = os.path.join(juju_home, 'models')
3274
    else:
3275
        root = os.path.join(juju_home, 'environments')
3276
    return os.path.join(root, 'cache.yaml')
1050.2.2 by Aaron Bentley
Retain config.yaml instead of .jenv for JES.
3277
3278
1242.3.16 by Aaron Bentley
Make agent-version omission client-dependent.
3279
def make_safe_config(client):
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
3280
    config = dict(client.env.config)
1242.3.18 by Aaron Bentley
Rename bootstrap_supports to bootstrap_replaces.
3281
    if 'agent-version' in client.bootstrap_replaces:
1242.3.16 by Aaron Bentley
Make agent-version omission client-dependent.
3282
        config.pop('agent-version', None)
3283
    else:
1242.3.10 by Aaron Bentley
Base support for agent-version.
3284
        config['agent-version'] = client.get_matching_agent_version()
709.1.1 by Aaron Bentley
Always set test-mode to True.
3285
    # AFAICT, we *always* want to set test-mode to True.  If we ever find a
3286
    # use-case where we don't, we can make this optional.
3287
    config['test-mode'] = True
990.3.1 by Curtis Hovey
Ensure the env name is in the config.
3288
    # Explicitly set 'name', which Juju implicitly sets to env.environment to
3289
    # ensure MAASAccount knows what the name will be.
1477.3.1 by Andrew Wilkins
beebop
3290
    config['name'] = unqualified_model_name(client.env.environment)
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
3291
    if config['type'] == 'local':
1193.2.5 by Aaron Bentley
Remove EnvJujuClient.juju_home completely.
3292
        config.setdefault('root-dir', get_local_root(client.env.juju_home,
995.1.14 by Aaron Bentley
Destroy environments.
3293
                          client.env))
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
3294
        # MongoDB requires a lot of free disk space, and the only
3295
        # visible error message is from "juju bootstrap":
3296
        # "cannot initiate replication set" if disk space is low.
3297
        # What "low" exactly means, is unclear, but 8GB should be
3298
        # enough.
3299
        ensure_dir(config['root-dir'])
3300
        check_free_disk_space(config['root-dir'], 8000000, "MongoDB files")
3301
        if client.env.kvm:
3302
            check_free_disk_space(
3303
                "/var/lib/uvtool/libvirt/images", 2000000,
3304
                "KVM disk files")
3305
        else:
3306
            check_free_disk_space(
3307
                "/var/lib/lxc", 2000000, "LXC containers")
995.1.13 by Aaron Bentley
Get assess_jes_deploy running to completion.
3308
    return config
3309
3310
3311
@contextmanager
1242.3.16 by Aaron Bentley
Make agent-version omission client-dependent.
3312
def temp_bootstrap_env(juju_home, client, set_home=True, permanent=False):
995.1.13 by Aaron Bentley
Get assess_jes_deploy running to completion.
3313
    """Create a temporary environment for bootstrapping.
3314
3315
    This involves creating a temporary juju home directory and returning its
3316
    location.
3317
1695.2.1 by Andrew Beach
Trying to get public-clouds.yaml into the temperary directory used for tests.
3318
    :param juju_home: The current JUJU_HOME value.
3319
    :param client: The client being prepared for bootstrapping.
995.1.13 by Aaron Bentley
Get assess_jes_deploy running to completion.
3320
    :param set_home: Set JUJU_HOME to match the temporary home in this
3321
        context.  If False, juju_home should be supplied to bootstrap.
1695.2.1 by Andrew Beach
Trying to get public-clouds.yaml into the temperary directory used for tests.
3322
    :param permanent: If permanent, the environment is kept afterwards.
1695.2.7 by Andrew Beach
Fixed a few bits of documentation.
3323
        Otherwise the environment is deleted when exiting the context.
995.1.13 by Aaron Bentley
Get assess_jes_deploy running to completion.
3324
    """
3325
    new_config = {
1251.2.6 by Aaron Bentley
Remove spurious change.
3326
        'environments': {client.env.environment: make_safe_config(client)}}
995.1.13 by Aaron Bentley
Get assess_jes_deploy running to completion.
3327
    # Always bootstrap a matching environment.
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
3328
    jenv_path = get_jenv_path(juju_home, client.env.environment)
1044.1.9 by Aaron Bentley
Update and add tests.
3329
    if permanent:
1260.2.6 by Aaron Bentley
Introduce JujuData, remove workarounds for cloud-credentials.
3330
        context = client.env.make_jes_home(
3331
            juju_home, client.env.environment, new_config)
1044.1.9 by Aaron Bentley
Update and add tests.
3332
    else:
3333
        context = _temp_env(new_config, juju_home, set_home)
3334
    with context as temp_juju_home:
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
3335
        if os.path.lexists(jenv_path):
3336
            raise Exception('%s already exists!' % jenv_path)
3337
        new_jenv_path = get_jenv_path(temp_juju_home, client.env.environment)
3338
        # Create a symlink to allow access while bootstrapping, and to reduce
3339
        # races.  Can't use a hard link because jenv doesn't exist until
3340
        # partway through bootstrap.
3341
        ensure_dir(os.path.join(juju_home, 'environments'))
1044.1.1 by Aaron Bentley
Use the deploy_job script everywhere, fix windows weirdness.
3342
        # Skip creating symlink where not supported (i.e. Windows).
1048.1.4 by Aaron Bentley
Improve condition order.
3343
        if not permanent and getattr(os, 'symlink', None) is not None:
1044.1.1 by Aaron Bentley
Use the deploy_job script everywhere, fix windows weirdness.
3344
            os.symlink(new_jenv_path, jenv_path)
1193.2.1 by Aaron Bentley
Make EnvJujuClient.juju_home a reference to EnvJUjuClient.env.juju_home.
3345
        old_juju_home = client.env.juju_home
3346
        client.env.juju_home = temp_juju_home
716.1.2 by Aaron Bentley
Extract _temp_env from temp_bootstrap_env.
3347
        try:
807.1.1 by Aaron Bentley
Avoid creating temp juju homes inside temp juju homes.
3348
            yield temp_juju_home
716.1.2 by Aaron Bentley
Extract _temp_env from temp_bootstrap_env.
3349
        finally:
1044.1.9 by Aaron Bentley
Update and add tests.
3350
            if not permanent:
3351
                # replace symlink with file before deleting temp home.
3352
                try:
3353
                    os.rename(new_jenv_path, jenv_path)
3354
                except OSError as e:
3355
                    if e.errno != errno.ENOENT:
3356
                        raise
3357
                    # Remove dangling symlink
1703.1.6 by Andrew Beach
Renamed the function to skip_on_missing_file.
3358
                    with skip_on_missing_file():
1048.1.5 by Aaron Bentley
Updates from review.
3359
                        os.unlink(jenv_path)
1193.2.1 by Aaron Bentley
Make EnvJujuClient.juju_home a reference to EnvJUjuClient.env.juju_home.
3360
                client.env.juju_home = old_juju_home
663.1.2 by Aaron Bentley
Port bootstrap_from_env to EnvJujuClient, add unit tests.
3361
3362
1153.4.3 by Martin Packman
Changes from review by abentley
3363
def get_machine_dns_name(client, machine, timeout=600):
3364
    """Wait for dns-name on a juju machine."""
3365
    for status in client.status_until(timeout=timeout):
3366
        try:
3367
            return _dns_name_for_machine(status, machine)
3368
        except KeyError:
1153.4.4 by Martin Packman
Log in jujupy context
3369
            log.debug("No dns-name yet for machine %s", machine)
1153.4.3 by Martin Packman
Changes from review by abentley
3370
3371
3372
def _dns_name_for_machine(status, machine):
3373
    host = status.status['machines'][machine]['dns-name']
1167.2.1 by Martin Packman
Support IPv6 in wait_for_port and move handling of literal addresses closer to final use
3374
    if is_ipv6_address(host):
3375
        log.warning("Selected IPv6 address for machine %s: %r", machine, host)
3376
    return host
1153.4.3 by Martin Packman
Changes from review by abentley
3377
3378
1315.2.22 by Aaron Bentley
Fake merge of trunk.
3379
class Controller:
1600.1.2 by Andrew Beach
Various spelling mistakes.
3380
    """Represents the controller for a model or models."""
1315.2.22 by Aaron Bentley
Fake merge of trunk.
3381
3382
    def __init__(self, name):
3383
        self.name = name
1711.3.3 by Aaron Bentley
Let controller decide whether to supply cloud/region and credentials.
3384
        self.explicit_region = False
1315.2.22 by Aaron Bentley
Fake merge of trunk.
3385
3386
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
3387
class GroupReporter:
3388
3389
    def __init__(self, stream, expected):
3390
        self.stream = stream
1196.1.6 by Martin Packman
Changes for review by abentley
3391
        self.expected = expected
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
3392
        self.last_group = None
3393
        self.ticks = 0
922.1.1 by Martin Packman
Add wrapping at 80 characters to group reporter
3394
        self.wrap_offset = 0
922.1.2 by Martin Packman
Use width of 79 and add test suggested by abentley in review
3395
        self.wrap_width = 79
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
3396
3397
    def _write(self, string):
3398
        self.stream.write(string)
3399
        self.stream.flush()
3400
769.1.1 by Martin Packman
Add finish method to GroupReporter to end dotties
3401
    def finish(self):
3402
        if self.last_group:
3403
            self._write("\n")
3404
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
3405
    def update(self, group):
3406
        if group == self.last_group:
922.1.1 by Martin Packman
Add wrapping at 80 characters to group reporter
3407
            if (self.wrap_offset + self.ticks) % self.wrap_width == 0:
3408
                self._write("\n")
3409
            self._write("." if self.ticks or not self.wrap_offset else " .")
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
3410
            self.ticks += 1
3411
            return
3412
        value_listing = []
3413
        for value, entries in sorted(group.items()):
1196.1.6 by Martin Packman
Changes for review by abentley
3414
            if value == self.expected:
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
3415
                continue
3416
            value_listing.append('%s: %s' % (value, ', '.join(entries)))
3417
        string = ' | '.join(value_listing)
930.1.1 by Martin Packman
Fix wrapping of dots when printing updated group status
3418
        lead_length = len(string) + 1
751.3.1 by Martin Packman
Use dots rather than repeated lines when waiting for status changes
3419
        if self.last_group:
3420
            string = "\n" + string
3421
        self._write(string)
3422
        self.last_group = group
3423
        self.ticks = 0
922.1.1 by Martin Packman
Add wrapping at 80 characters to group reporter
3424
        self.wrap_offset = lead_length if lead_length < self.wrap_width else 0