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

« back to all changes in this revision

Viewing changes to jujupy.py

  • Committer: Curtis Hovey
  • Date: 2016-06-15 20:52:35 UTC
  • Revision ID: curtis@canonical.com-20160615205235-cf6hu9xt1qmbo1a4
Escape vars in run-unit-tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
10
10
)
11
11
from copy import deepcopy
12
12
from cStringIO import StringIO
13
 
from datetime import datetime
 
13
from datetime import timedelta
14
14
import errno
15
15
from itertools import chain
16
16
import json
40
40
    JujuResourceTimeout,
41
41
    pause,
42
42
    quote,
43
 
    qualified_model_name,
44
43
    scoped_environ,
45
44
    split_address_port,
46
45
    temp_dir,
47
 
    unqualified_model_name,
48
46
    until_timeout,
49
47
)
50
48
 
78
76
log = logging.getLogger("jujupy")
79
77
 
80
78
 
81
 
class IncompatibleConfigClass(Exception):
82
 
    """Raised when a client is initialised with the wrong config class."""
83
 
 
84
 
 
85
 
class SoftDeadlineExceeded(Exception):
86
 
    """Raised when an overall client operation takes too long."""
87
 
 
88
 
 
89
79
def get_timeout_path():
90
80
    import timeout
91
81
    return os.path.abspath(timeout.__file__)
98
88
    return (sys.executable, timeout_path, '%.2f' % duration, '--')
99
89
 
100
90
 
101
 
def get_teardown_timeout(client):
102
 
    """Return the timeout need byt the client to teardown resources."""
103
 
    if client.env.config['type'] == 'azure':
104
 
        return 1800
105
 
    else:
106
 
        return 600
107
 
 
108
 
 
109
91
def parse_new_state_server_from_error(error):
110
92
    err_str = str(error)
111
93
    output = getattr(error, 'output', None)
174
156
    return state
175
157
 
176
158
 
 
159
def make_client(juju_path, debug, env_name, temp_env_name):
 
160
    env = SimpleEnvironment.from_config(env_name)
 
161
    if temp_env_name is not None:
 
162
        env.set_model_name(temp_env_name)
 
163
    return EnvJujuClient.by_version(env, juju_path, debug)
 
164
 
 
165
 
177
166
class CannotConnectEnv(subprocess.CalledProcessError):
178
167
 
179
168
    def __init__(self, e):
218
207
        os.unlink(temp_file.name)
219
208
 
220
209
 
221
 
class SimpleEnvironment:
222
 
    """Represents a model in a JUJU_HOME directory for juju 1."""
223
 
 
224
 
    def __init__(self, environment, config=None, juju_home=None,
225
 
                 controller=None):
226
 
        """Constructor.
227
 
 
228
 
        :param environment: Name of the environment.
229
 
        :param config: Dictionary with configuration options, default is None.
230
 
        :param juju_home: Path to JUJU_HOME directory, default is None.
231
 
        :param controller: Controller instance-- this model's controller.
232
 
            If not given or None a new instance is created."""
233
 
        self.user_name = None
234
 
        if controller is None:
235
 
            controller = Controller(environment)
236
 
        self.controller = controller
237
 
        self.environment = environment
238
 
        self.config = config
239
 
        self.juju_home = juju_home
240
 
        if self.config is not None:
241
 
            self.local = bool(self.config.get('type') == 'local')
242
 
            self.kvm = (
243
 
                self.local and bool(self.config.get('container') == 'kvm'))
244
 
            self.maas = bool(self.config.get('type') == 'maas')
245
 
            self.joyent = bool(self.config.get('type') == 'joyent')
246
 
        else:
247
 
            self.local = False
248
 
            self.kvm = False
249
 
            self.maas = False
250
 
            self.joyent = False
251
 
 
252
 
    def clone(self, model_name=None):
253
 
        config = deepcopy(self.config)
254
 
        if model_name is None:
255
 
            model_name = self.environment
256
 
        else:
257
 
            config['name'] = unqualified_model_name(model_name)
258
 
        result = self.__class__(model_name, config, self.juju_home,
259
 
                                self.controller)
260
 
        result.local = self.local
261
 
        result.kvm = self.kvm
262
 
        result.maas = self.maas
263
 
        result.joyent = self.joyent
264
 
        return result
265
 
 
266
 
    def __eq__(self, other):
267
 
        if type(self) != type(other):
268
 
            return False
269
 
        if self.environment != other.environment:
270
 
            return False
271
 
        if self.config != other.config:
272
 
            return False
273
 
        if self.local != other.local:
274
 
            return False
275
 
        if self.maas != other.maas:
276
 
            return False
277
 
        return True
278
 
 
279
 
    def __ne__(self, other):
280
 
        return not self == other
281
 
 
282
 
    def set_model_name(self, model_name, set_controller=True):
283
 
        if set_controller:
284
 
            self.controller.name = model_name
285
 
        self.environment = model_name
286
 
        self.config['name'] = unqualified_model_name(model_name)
287
 
 
288
 
    @classmethod
289
 
    def from_config(cls, name):
290
 
        """Create an environment from the configuation file.
291
 
 
292
 
        :param name: Name of the environment to get the configuration from."""
293
 
        return cls._from_config(name)
294
 
 
295
 
    @classmethod
296
 
    def _from_config(cls, name):
297
 
        config, selected = get_selected_environment(name)
298
 
        if name is None:
299
 
            name = selected
300
 
        return cls(name, config)
301
 
 
302
 
    def needs_sudo(self):
303
 
        return self.local
304
 
 
305
 
    @contextmanager
306
 
    def make_jes_home(self, juju_home, dir_name, new_config):
307
 
        home_path = jes_home_path(juju_home, dir_name)
308
 
        if os.path.exists(home_path):
309
 
            rmtree(home_path)
310
 
        os.makedirs(home_path)
311
 
        self.dump_yaml(home_path, new_config)
312
 
        yield home_path
313
 
 
314
 
    def get_cloud_credentials(self):
315
 
        """Return the credentials for this model's cloud.
316
 
 
317
 
        This implementation returns config variables in addition to
318
 
        credentials.
319
 
        """
320
 
        return self.config
321
 
 
322
 
    def dump_yaml(self, path, config):
323
 
        dump_environments_yaml(path, config)
324
 
 
325
 
 
326
 
class JujuData(SimpleEnvironment):
327
 
    """Represents a model in a JUJU_DATA directory for juju 2."""
328
 
 
329
 
    def __init__(self, environment, config=None, juju_home=None,
330
 
                 controller=None):
331
 
        """Constructor.
332
 
 
333
 
        This extends SimpleEnvironment's constructor.
334
 
 
335
 
        :param environment: Name of the environment.
336
 
        :param config: Dictionary with configuration options; default is None.
337
 
        :param juju_home: Path to JUJU_DATA directory. If None (the default),
338
 
            the home directory is autodetected.
339
 
        :param controller: Controller instance-- this model's controller.
340
 
            If not given or None, a new instance is created.
341
 
        """
342
 
        if juju_home is None:
343
 
            juju_home = get_juju_home()
344
 
        super(JujuData, self).__init__(environment, config, juju_home,
345
 
                                       controller)
346
 
        self.credentials = {}
347
 
        self.clouds = {}
348
 
 
349
 
    def clone(self, model_name=None):
350
 
        result = super(JujuData, self).clone(model_name)
351
 
        result.credentials = deepcopy(self.credentials)
352
 
        result.clouds = deepcopy(self.clouds)
353
 
        return result
354
 
 
355
 
    @classmethod
356
 
    def from_env(cls, env):
357
 
        juju_data = cls(env.environment, env.config, env.juju_home)
358
 
        juju_data.load_yaml()
359
 
        return juju_data
360
 
 
361
 
    def load_yaml(self):
362
 
        try:
363
 
            with open(os.path.join(self.juju_home, 'credentials.yaml')) as f:
364
 
                self.credentials = yaml.safe_load(f)
365
 
        except IOError as e:
366
 
            if e.errno != errno.ENOENT:
367
 
                raise RuntimeError(
368
 
                    'Failed to read credentials file: {}'.format(str(e)))
369
 
            self.credentials = {}
370
 
        try:
371
 
            with open(os.path.join(self.juju_home, 'clouds.yaml')) as f:
372
 
                self.clouds = yaml.safe_load(f)
373
 
        except IOError as e:
374
 
            if e.errno != errno.ENOENT:
375
 
                raise RuntimeError(
376
 
                    'Failed to read clouds file: {}'.format(str(e)))
377
 
            # Default to an empty clouds file.
378
 
            self.clouds = {'clouds': {}}
379
 
 
380
 
    @classmethod
381
 
    def from_config(cls, name):
382
 
        """Create a model from the three configuration files."""
383
 
        juju_data = cls._from_config(name)
384
 
        juju_data.load_yaml()
385
 
        return juju_data
386
 
 
387
 
    def dump_yaml(self, path, config):
388
 
        """Dump the configuration files to the specified path.
389
 
 
390
 
        config is unused, but is accepted for compatibility with
391
 
        SimpleEnvironment and make_jes_home().
392
 
        """
393
 
        with open(os.path.join(path, 'credentials.yaml'), 'w') as f:
394
 
            yaml.safe_dump(self.credentials, f)
395
 
        with open(os.path.join(path, 'clouds.yaml'), 'w') as f:
396
 
            yaml.safe_dump(self.clouds, f)
397
 
 
398
 
    def find_endpoint_cloud(self, cloud_type, endpoint):
399
 
        for cloud, cloud_config in self.clouds['clouds'].items():
400
 
            if cloud_config['type'] != cloud_type:
401
 
                continue
402
 
            if cloud_config['endpoint'] == endpoint:
403
 
                return cloud
404
 
        raise LookupError('No such endpoint: {}'.format(endpoint))
405
 
 
406
 
    def get_cloud(self):
407
 
        provider = self.config['type']
408
 
        # Separate cloud recommended by: Juju Cloud / Credentials / BootStrap /
409
 
        # Model CLI specification
410
 
        if provider == 'ec2' and self.config['region'] == 'cn-north-1':
411
 
            return 'aws-china'
412
 
        if provider not in ('maas', 'openstack'):
413
 
            return {
414
 
                'ec2': 'aws',
415
 
                'gce': 'google',
416
 
            }.get(provider, provider)
417
 
        if provider == 'maas':
418
 
            endpoint = self.config['maas-server']
419
 
        elif provider == 'openstack':
420
 
            endpoint = self.config['auth-url']
421
 
        return self.find_endpoint_cloud(provider, endpoint)
422
 
 
423
 
    def get_region(self):
424
 
        provider = self.config['type']
425
 
        if provider == 'azure':
426
 
            if 'tenant-id' not in self.config:
427
 
                return self.config['location'].replace(' ', '').lower()
428
 
            return self.config['location']
429
 
        elif provider == 'joyent':
430
 
            matcher = re.compile('https://(.*).api.joyentcloud.com')
431
 
            return matcher.match(self.config['sdc-url']).group(1)
432
 
        elif provider == 'lxd':
433
 
            return 'localhost'
434
 
        elif provider == 'manual':
435
 
            return self.config['bootstrap-host']
436
 
        elif provider in ('maas', 'manual'):
437
 
            return None
438
 
        else:
439
 
            return self.config['region']
440
 
 
441
 
    def get_cloud_credentials(self):
442
 
        """Return the credentials for this model's cloud."""
443
 
        cloud_name = self.get_cloud()
444
 
        cloud = self.credentials['credentials'][cloud_name]
445
 
        (credentials,) = cloud.values()
446
 
        return credentials
447
 
 
448
 
 
449
210
class Status:
450
211
 
451
212
    def __init__(self, status, status_text):
583
344
    Uses -m to specify models, uses JUJU_DATA to specify home directory.
584
345
    """
585
346
 
586
 
    def __init__(self, full_path, version, feature_flags, debug,
587
 
                 soft_deadline=None):
 
347
    def __init__(self, full_path, version, feature_flags, debug):
588
348
        self._version = version
589
349
        self._full_path = full_path
590
350
        self.feature_flags = feature_flags
591
351
        self.debug = debug
592
352
        self._timeout_path = get_timeout_path()
593
353
        self.juju_timings = {}
594
 
        self.soft_deadline = soft_deadline
595
 
        self._ignore_soft_deadline = False
596
 
 
597
 
    def _now(self):
598
 
        return datetime.utcnow()
599
 
 
600
 
    @contextmanager
601
 
    def _check_timeouts(self):
602
 
        # If an exception occurred, we don't want to replace it with
603
 
        # SoftDeadlineExceeded.
604
 
        yield
605
 
        if self.soft_deadline is None or self._ignore_soft_deadline:
606
 
            return
607
 
        if self._now() > self.soft_deadline:
608
 
            raise SoftDeadlineExceeded('Operation exceeded deadline.')
609
 
 
610
 
    @contextmanager
611
 
    def ignore_soft_deadline(self):
612
 
        """Ignore the client deadline.  For cleanup code."""
613
 
        old_val = self._ignore_soft_deadline
614
 
        self._ignore_soft_deadline = True
615
 
        try:
616
 
            yield
617
 
        finally:
618
 
            self._ignore_soft_deadline = old_val
619
354
 
620
355
    def clone(self, full_path, version, debug, feature_flags):
621
356
        if version is None:
624
359
            full_path = self.full_path
625
360
        if debug is None:
626
361
            debug = self.debug
627
 
        result = self.__class__(full_path, version, feature_flags, debug,
628
 
                                self.soft_deadline)
 
362
        result = self.__class__(full_path, version, feature_flags, debug)
629
363
        return result
630
364
 
631
365
    @property
702
436
        # Mutate os.environ instead of supplying env parameter so Windows can
703
437
        # search env['PATH']
704
438
        with scoped_environ(env):
705
 
            with self._check_timeouts():
706
 
                rval = call_func(args)
 
439
            rval = call_func(args)
707
440
        self.juju_timings.setdefault(args, []).append(
708
441
            (time.time() - start_time))
709
442
        return rval
731
464
        # Mutate os.environ instead of supplying env parameter so Windows can
732
465
        # search env['PATH']
733
466
        with scoped_environ(env):
734
 
            with self._check_timeouts():
735
 
                proc = subprocess.Popen(full_args)
 
467
            proc = subprocess.Popen(full_args)
736
468
        yield proc
737
469
        retcode = proc.wait()
738
470
        if retcode != 0:
739
471
            raise subprocess.CalledProcessError(retcode, full_args)
740
472
 
741
 
    def get_juju_output(self, command, args, used_feature_flags, juju_home,
742
 
                        model=None, timeout=None, user_name=None,
743
 
                        merge_stderr=False):
 
473
    def get_juju_output(self, command, args, used_feature_flags,
 
474
                        juju_home, model=None, timeout=None):
744
475
        args = self.full_args(command, args, model, timeout)
745
476
        env = self.shell_environ(used_feature_flags, juju_home)
746
477
        log.debug(args)
749
480
        with scoped_environ(env):
750
481
            proc = subprocess.Popen(
751
482
                args, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
752
 
                stderr=subprocess.STDOUT if merge_stderr else subprocess.PIPE)
753
 
            with self._check_timeouts():
754
 
                sub_output, sub_error = proc.communicate()
 
483
                stderr=subprocess.PIPE)
 
484
            sub_output, sub_error = proc.communicate()
755
485
            log.debug(sub_output)
756
486
            if proc.returncode != 0:
757
487
                log.debug(sub_error)
758
488
                e = subprocess.CalledProcessError(
759
489
                    proc.returncode, args, sub_output)
760
490
                e.stderr = sub_error
761
 
                if sub_error and (
 
491
                if (
762
492
                    'Unable to connect to environment' in sub_error or
763
493
                        'MissingOrIncorrectVersionHeader' in sub_error or
764
494
                        '307: Temporary Redirect' in sub_error):
830
560
                args)
831
561
 
832
562
 
833
 
def get_client_class(version):
834
 
    if version.startswith('1.16'):
835
 
        raise Exception('Unsupported juju: %s' % version)
836
 
    elif re.match('^1\.22[.-]', version):
837
 
        client_class = EnvJujuClient22
838
 
    elif re.match('^1\.24[.-]', version):
839
 
        client_class = EnvJujuClient24
840
 
    elif re.match('^1\.25[.-]', version):
841
 
        client_class = EnvJujuClient25
842
 
    elif re.match('^1\.26[.-]', version):
843
 
        client_class = EnvJujuClient26
844
 
    elif re.match('^1\.', version):
845
 
        client_class = EnvJujuClient1X
846
 
    # Ensure alpha/beta number matches precisely
847
 
    elif re.match('^2\.0-alpha1([^\d]|$)', version):
848
 
        client_class = EnvJujuClient2A1
849
 
    elif re.match('^2\.0-alpha2([^\d]|$)', version):
850
 
        client_class = EnvJujuClient2A2
851
 
    elif re.match('^2\.0-(alpha3|beta[12])([^\d]|$)', version):
852
 
        client_class = EnvJujuClient2B2
853
 
    elif re.match('^2\.0-(beta[3-6])([^\d]|$)', version):
854
 
        client_class = EnvJujuClient2B3
855
 
    elif re.match('^2\.0-(beta7)([^\d]|$)', version):
856
 
        client_class = EnvJujuClient2B7
857
 
    elif re.match('^2\.0-beta8([^\d]|$)', version):
858
 
        client_class = EnvJujuClient2B8
859
 
    # between beta 9-14
860
 
    elif re.match('^2\.0-beta(9|1[0-4])([^\d]|$)', version):
861
 
        client_class = EnvJujuClient2B9
862
 
    else:
863
 
        client_class = EnvJujuClient
864
 
    return client_class
865
 
 
866
 
 
867
 
def client_from_config(config, juju_path, debug=False, soft_deadline=None):
868
 
    """Create a client from an environment's configuration.
869
 
 
870
 
    :param config: Name of the environment to use the config from.
871
 
    :param juju_path: Path to juju binary the client should wrap.
872
 
    :param debug=False: The debug flag for the client, False by default.
873
 
    :param soft_deadline: A datetime representing the deadline by which
874
 
        normal operations should complete.  If None, no deadline is
875
 
        enforced.
876
 
    """
877
 
    version = EnvJujuClient.get_version(juju_path)
878
 
    client_class = get_client_class(version)
879
 
    env = client_class.config_class.from_config(config)
880
 
    if juju_path is None:
881
 
        full_path = EnvJujuClient.get_full_path()
882
 
    else:
883
 
        full_path = os.path.abspath(juju_path)
884
 
    return client_class(env, version, full_path, debug=debug,
885
 
                        soft_deadline=soft_deadline)
886
 
 
887
 
 
888
563
class EnvJujuClient:
889
 
    """Wraps calls to a juju instance, associated with a single model.
890
 
 
891
 
    Note: A model is often called an enviroment (Juju 1 legacy).
892
 
 
893
 
    This class represents the latest Juju version.  Subclasses are used to
894
 
    support older versions (see get_client_class).
895
 
    """
896
564
 
897
565
    # The environments.yaml options that are replaced by bootstrap options.
898
566
    #
915
583
 
916
584
    default_backend = Juju2Backend
917
585
 
918
 
    config_class = JujuData
919
 
 
920
586
    status_class = Status
921
587
 
922
 
    agent_metadata_url = 'agent-metadata-url'
923
 
 
924
 
    model_permissions = frozenset(['read', 'write', 'admin'])
925
 
 
926
 
    controller_permissions = frozenset(['login', 'addmodel', 'superuser'])
927
 
 
928
588
    @classmethod
929
589
    def preferred_container(cls):
930
590
        for container_type in [LXD_MACHINE, LXC_MACHINE]:
935
595
 
936
596
    @classmethod
937
597
    def get_version(cls, juju_path=None):
938
 
        """Get the version data from a juju binary.
939
 
 
940
 
        :param juju_path: Path to binary. If not given or None, 'juju' is used.
941
 
        """
942
598
        if juju_path is None:
943
599
            juju_path = 'juju'
944
600
        return subprocess.check_output((juju_path, '--version')).strip()
945
601
 
946
 
    def check_timeouts(self):
947
 
        return self._backend._check_timeouts()
948
 
 
949
 
    def ignore_soft_deadline(self):
950
 
        return self._backend.ignore_soft_deadline()
951
 
 
952
602
    def enable_feature(self, flag):
953
603
        """Enable juju feature by setting the given flag.
954
604
 
993
643
            return WIN_JUJU_CMD
994
644
        return subprocess.check_output(('which', 'juju')).rstrip('\n')
995
645
 
996
 
    def clone_path_cls(self, juju_path):
997
 
        """Clone using the supplied path to determine the class."""
998
 
        version = self.get_version(juju_path)
999
 
        cls = get_client_class(version)
 
646
    @classmethod
 
647
    def by_version(cls, env, juju_path=None, debug=False):
 
648
        version = cls.get_version(juju_path)
1000
649
        if juju_path is None:
1001
 
            full_path = self.get_full_path()
 
650
            full_path = cls.get_full_path()
1002
651
        else:
1003
652
            full_path = os.path.abspath(juju_path)
1004
 
        return self.clone(version=version, full_path=full_path, cls=cls)
 
653
        if version.startswith('1.16'):
 
654
            raise Exception('Unsupported juju: %s' % version)
 
655
        elif re.match('^1\.22[.-]', version):
 
656
            client_class = EnvJujuClient22
 
657
        elif re.match('^1\.24[.-]', version):
 
658
            client_class = EnvJujuClient24
 
659
        elif re.match('^1\.25[.-]', version):
 
660
            client_class = EnvJujuClient25
 
661
        elif re.match('^1\.26[.-]', version):
 
662
            client_class = EnvJujuClient26
 
663
        elif re.match('^1\.', version):
 
664
            client_class = EnvJujuClient1X
 
665
        elif re.match('^2\.0-alpha1', version):
 
666
            client_class = EnvJujuClient2A1
 
667
        elif re.match('^2\.0-alpha2', version):
 
668
            client_class = EnvJujuClient2A2
 
669
        elif re.match('^2\.0-(alpha3|beta[12])', version):
 
670
            client_class = EnvJujuClient2B2
 
671
        elif re.match('^2\.0-(beta[3-6])', version):
 
672
            client_class = EnvJujuClient2B3
 
673
        elif re.match('^2\.0-(beta7)', version):
 
674
            client_class = EnvJujuClient2B7
 
675
        elif re.match('^2\.0-beta8', version):
 
676
            client_class = EnvJujuClient2B8
 
677
        else:
 
678
            client_class = EnvJujuClient
 
679
        return client_class(env, version, full_path, debug=debug)
1005
680
 
1006
681
    def clone(self, env=None, version=None, full_path=None, debug=None,
1007
682
              cls=None):
1028
703
    def get_cache_path(self):
1029
704
        return get_cache_path(self.env.juju_home, models=True)
1030
705
 
1031
 
    def _cmd_model(self, include_e, controller):
1032
 
        if controller:
 
706
    def _cmd_model(self, include_e, admin):
 
707
        if admin:
1033
708
            return '{controller}:{model}'.format(
1034
709
                controller=self.env.controller.name,
1035
 
                model=self.get_controller_model_name())
 
710
                model=self.get_admin_model_name())
1036
711
        elif self.env is None or not include_e:
1037
712
            return None
1038
713
        else:
1041
716
                model=self.model_name)
1042
717
 
1043
718
    def _full_args(self, command, sudo, args,
1044
 
                   timeout=None, include_e=True, controller=False):
1045
 
        model = self._cmd_model(include_e, controller)
 
719
                   timeout=None, include_e=True, admin=False):
 
720
        model = self._cmd_model(include_e, admin)
1046
721
        # sudo is not needed for devel releases.
1047
722
        return self._backend.full_args(command, args, model, timeout)
1048
723
 
1055
730
        return env
1056
731
 
1057
732
    def __init__(self, env, version, full_path, juju_home=None, debug=False,
1058
 
                 soft_deadline=None, _backend=None):
1059
 
        """Create a new juju client.
1060
 
 
1061
 
        Required Arguments
1062
 
        :param env: Object representing a model in a data directory.
1063
 
        :param version: Version of juju the client wraps.
1064
 
        :param full_path: Full path to juju binary.
1065
 
 
1066
 
        Optional Arguments
1067
 
        :param juju_home: default value for env.juju_home.  Will be
1068
 
            autodetected if None (the default).
1069
 
        :param debug: Flag to activate debugging output; False by default.
1070
 
        :param soft_deadline: A datetime representing the deadline by which
1071
 
            normal operations should complete.  If None, no deadline is
1072
 
            enforced.
1073
 
        :param _backend: The backend to use for interacting with the client.
1074
 
            If None (the default), self.default_backend will be used.
1075
 
        """
 
733
                 _backend=None):
1076
734
        self.env = self._get_env(env)
1077
735
        if _backend is None:
1078
 
            _backend = self.default_backend(full_path, version, set(), debug,
1079
 
                                            soft_deadline)
 
736
            _backend = self.default_backend(full_path, version, set(), debug)
1080
737
        self._backend = _backend
1081
738
        if version != _backend.version:
1082
739
            raise ValueError('Version mismatch: {} {}'.format(
1143
800
            return cloud
1144
801
        return '{}/{}'.format(cloud, region)
1145
802
 
1146
 
    def get_bootstrap_args(
1147
 
            self, upload_tools, config_filename, bootstrap_series=None,
1148
 
            credential=None, auto_upgrade=False, metadata_source=None,
1149
 
            to=None, agent_version=None):
 
803
    def get_bootstrap_args(self, upload_tools, config_filename,
 
804
                           bootstrap_series=None):
1150
805
        """Return the bootstrap arguments for the substrate."""
1151
 
        if self.env.joyent:
 
806
        if self.env.maas:
 
807
            constraints = 'mem=2G arch=amd64'
 
808
        elif self.env.joyent:
1152
809
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1153
810
            constraints = 'mem=2G cpu-cores=1'
1154
811
        else:
1159
816
                cloud_region, '--config', config_filename,
1160
817
                '--default-model', self.env.environment]
1161
818
        if upload_tools:
1162
 
            if agent_version is not None:
1163
 
                raise ValueError(
1164
 
                    'agent-version may not be given with upload-tools.')
1165
819
            args.insert(0, '--upload-tools')
1166
820
        else:
1167
 
            if agent_version is None:
1168
 
                agent_version = self.get_matching_agent_version()
1169
 
            args.extend(['--agent-version', agent_version])
 
821
            args.extend(['--agent-version', self.get_matching_agent_version()])
 
822
 
1170
823
        if bootstrap_series is not None:
1171
824
            args.extend(['--bootstrap-series', bootstrap_series])
1172
 
        if credential is not None:
1173
 
            args.extend(['--credential', credential])
1174
 
        if metadata_source is not None:
1175
 
            args.extend(['--metadata-source', metadata_source])
1176
 
        if auto_upgrade:
1177
 
            args.append('--auto-upgrade')
1178
 
        if to is not None:
1179
 
            args.extend(['--to', to])
1180
825
        return tuple(args)
1181
826
 
1182
827
    def add_model(self, env):
1183
 
        """Add a model to this model's controller and return its client.
1184
 
 
1185
 
        :param env: Class representing the new model/environment."""
1186
828
        model_client = self.clone(env)
1187
829
        with model_client._bootstrap_config() as config_file:
1188
830
            self._add_model(env.environment, config_file)
1237
879
            raise AssertionError(
1238
880
                'Controller and environment names should not vary (yet)')
1239
881
 
1240
 
    def update_user_name(self):
1241
 
        self.env.user_name = 'admin@local'
1242
 
 
1243
 
    def bootstrap(self, upload_tools=False, bootstrap_series=None,
1244
 
                  credential=None, auto_upgrade=False, metadata_source=None,
1245
 
                  to=None, agent_version=None):
 
882
    def bootstrap(self, upload_tools=False, bootstrap_series=None):
1246
883
        """Bootstrap a controller."""
1247
884
        self._check_bootstrap()
1248
885
        with self._bootstrap_config() as config_filename:
1249
886
            args = self.get_bootstrap_args(
1250
 
                upload_tools, config_filename, bootstrap_series, credential,
1251
 
                auto_upgrade, metadata_source, to, agent_version)
1252
 
            self.update_user_name()
 
887
                upload_tools, config_filename, bootstrap_series)
1253
888
            self.juju('bootstrap', args, include_e=False)
1254
889
 
1255
890
    @contextmanager
1256
 
    def bootstrap_async(self, upload_tools=False, bootstrap_series=None,
1257
 
                        auto_upgrade=False, metadata_source=None, to=None):
 
891
    def bootstrap_async(self, upload_tools=False, bootstrap_series=None):
1258
892
        self._check_bootstrap()
1259
893
        with self._bootstrap_config() as config_filename:
1260
894
            args = self.get_bootstrap_args(
1261
 
                upload_tools, config_filename, bootstrap_series, None,
1262
 
                auto_upgrade, metadata_source, to)
1263
 
            self.update_user_name()
 
895
                upload_tools, config_filename, bootstrap_series)
1264
896
            with self.juju_async('bootstrap', args, include_e=False):
1265
897
                yield
1266
898
                log.info('Waiting for bootstrap of {}.'.format(
1273
905
    def destroy_model(self):
1274
906
        exit_status = self.juju(
1275
907
            'destroy-model', (self.env.environment, '-y',),
1276
 
            include_e=False, timeout=get_teardown_timeout(self))
 
908
            include_e=False, timeout=timedelta(minutes=10).total_seconds())
1277
909
        return exit_status
1278
910
 
1279
911
    def kill_controller(self):
1281
913
        seen_cmd = self.get_jes_command()
1282
914
        self.juju(
1283
915
            _jes_cmds[seen_cmd]['kill'], (self.env.controller.name, '-y'),
1284
 
            include_e=False, check=False, timeout=get_teardown_timeout(self))
 
916
            include_e=False, check=False, timeout=600)
1285
917
 
1286
918
    def get_juju_output(self, command, *args, **kwargs):
1287
919
        """Call a juju command and return the output.
1291
923
        <environment> flag will be placed after <command> and before args.
1292
924
        """
1293
925
        model = self._cmd_model(kwargs.get('include_e', True),
1294
 
                                kwargs.get('controller', False))
1295
 
        pass_kwargs = dict(
1296
 
            (k, kwargs[k]) for k in kwargs if k in ['timeout', 'merge_stderr'])
 
926
                                kwargs.get('admin', False))
 
927
        timeout = kwargs.get('timeout')
1297
928
        return self._backend.get_juju_output(
1298
929
            command, args, self.used_feature_flags, self.env.juju_home,
1299
 
            model, user_name=self.env.user_name, **pass_kwargs)
 
930
            model, timeout)
1300
931
 
1301
932
    def show_status(self):
1302
933
        """Print the status to output."""
1303
934
        self.juju(self._show_status, ('--format', 'yaml'))
1304
935
 
1305
 
    def get_status(self, timeout=60, raw=False, controller=False, *args):
 
936
    def get_status(self, timeout=60, raw=False, admin=False, *args):
1306
937
        """Get the current status as a dict."""
1307
938
        # GZ 2015-12-16: Pass remaining timeout into get_juju_output call.
1308
939
        for ignored in until_timeout(timeout):
1311
942
                    return self.get_juju_output(self._show_status, *args)
1312
943
                return self.status_class.from_text(
1313
944
                    self.get_juju_output(
1314
 
                        self._show_status, '--format', 'yaml',
1315
 
                        controller=controller))
 
945
                        self._show_status, '--format', 'yaml', admin=admin))
1316
946
            except subprocess.CalledProcessError:
1317
947
                pass
1318
948
        raise Exception(
1324
954
 
1325
955
    def set_config(self, service, options):
1326
956
        option_strings = self._dict_as_option_strings(options)
1327
 
        self.juju('config', (service,) + option_strings)
 
957
        self.juju('set-config', (service,) + option_strings)
1328
958
 
1329
959
    def get_config(self, service):
1330
 
        return yaml_loads(self.get_juju_output('config', service))
 
960
        return yaml_loads(self.get_juju_output('get-config', service))
1331
961
 
1332
962
    def get_service_config(self, service, timeout=60):
1333
963
        for ignored in until_timeout(timeout):
1343
973
        return self.juju('set-model-constraints', constraint_strings)
1344
974
 
1345
975
    def get_model_config(self):
1346
 
        """Return the value of the environment's configured options."""
1347
 
        return yaml.safe_load(
1348
 
            self.get_juju_output('model-config', '--format', 'yaml'))
 
976
        """Return the value of the environment's configured option."""
 
977
        return yaml.safe_load(self.get_juju_output('get-model-config'))
1349
978
 
1350
979
    def get_env_option(self, option):
1351
980
        """Return the value of the environment's configured option."""
1352
 
        return self.get_juju_output('model-config', option)
 
981
        return self.get_juju_output('get-model-config', option)
1353
982
 
1354
983
    def set_env_option(self, option, value):
1355
984
        """Set the value of the option in the environment."""
1356
985
        option_value = "%s=%s" % (option, value)
1357
 
        return self.juju('model-config', (option_value,))
1358
 
 
1359
 
    def unset_env_option(self, option):
1360
 
        """Unset the value of the option in the environment."""
1361
 
        return self.juju('model-config', ('--reset', option,))
1362
 
 
1363
 
    def get_agent_metadata_url(self):
1364
 
        return self.get_env_option(self.agent_metadata_url)
1365
 
 
1366
 
    def set_testing_agent_metadata_url(self):
1367
 
        url = self.get_agent_metadata_url()
 
986
        return self.juju('set-model-config', (option_value,))
 
987
 
 
988
    def set_testing_tools_metadata_url(self):
 
989
        url = self.get_env_option('tools-metadata-url')
1368
990
        if 'testing' not in url:
1369
991
            testing_url = url.replace('/tools', '/testing/tools')
1370
 
            self.set_env_option(self.agent_metadata_url, testing_url)
 
992
            self.set_env_option('tools-metadata-url', testing_url)
1371
993
 
1372
994
    def juju(self, command, args, sudo=False, check=True, include_e=True,
1373
995
             timeout=None, extra_env=None):
1374
996
        """Run a command under juju for the current environment."""
1375
 
        model = self._cmd_model(include_e, controller=False)
 
997
        model = self._cmd_model(include_e, admin=False)
1376
998
        return self._backend.juju(
1377
999
            command, args, self.used_feature_flags, self.env.juju_home,
1378
1000
            model, check, timeout, extra_env)
1395
1017
          `args`.
1396
1018
 
1397
1019
        """
1398
 
        model = self._cmd_model(include_e, controller=False)
 
1020
        model = self._cmd_model(include_e, admin=False)
1399
1021
        return self._backend.expect(
1400
1022
            command, args, self.used_feature_flags, self.env.juju_home,
1401
1023
            model, timeout, extra_env)
1411
1033
        return stringified_timings
1412
1034
 
1413
1035
    def juju_async(self, command, args, include_e=True, timeout=None):
1414
 
        model = self._cmd_model(include_e, controller=False)
 
1036
        model = self._cmd_model(include_e, admin=False)
1415
1037
        return self._backend.juju_async(command, args, self.used_feature_flags,
1416
1038
                                        self.env.juju_home, model, timeout)
1417
1039
 
1418
1040
    def deploy(self, charm, repository=None, to=None, series=None,
1419
 
               service=None, force=False, resource=None,
1420
 
               storage=None, constraints=None):
 
1041
               service=None, force=False, resource=None, storage=None):
1421
1042
        args = [charm]
1422
1043
        if service is not None:
1423
1044
            args.extend([service])
1431
1052
            args.extend(['--resource', resource])
1432
1053
        if storage is not None:
1433
1054
            args.extend(['--storage', storage])
1434
 
        if constraints is not None:
1435
 
            args.extend(['--constraints', constraints])
1436
1055
        return self.juju('deploy', tuple(args))
1437
1056
 
1438
1057
    def attach(self, service, resource):
1447
1066
 
1448
1067
    def wait_for_resource(self, resource_id, service_or_unit, timeout=60):
1449
1068
        log.info('Waiting for resource. Resource id:{}'.format(resource_id))
1450
 
        with self.check_timeouts():
1451
 
            with self.ignore_soft_deadline():
1452
 
                for _ in until_timeout(timeout):
1453
 
                    resources_dict = self.list_resources(service_or_unit)
1454
 
                    resources = resources_dict['resources']
1455
 
                    for resource in resources:
1456
 
                        if resource['expected']['resourceid'] == resource_id:
1457
 
                            if (resource['expected']['fingerprint'] ==
1458
 
                                    resource['unit']['fingerprint']):
1459
 
                                return
1460
 
                    time.sleep(.1)
1461
 
                raise JujuResourceTimeout(
1462
 
                    'Timeout waiting for a resource to be downloaded. '
1463
 
                    'ResourceId: {} Service or Unit: {} Timeout: {}'.format(
1464
 
                        resource_id, service_or_unit, timeout))
 
1069
        for _ in until_timeout(timeout):
 
1070
            resources = self.list_resources(service_or_unit)['resources']
 
1071
            for resource in resources:
 
1072
                if resource['expected']['resourceid'] == resource_id:
 
1073
                    if (resource['expected']['fingerprint'] ==
 
1074
                            resource['unit']['fingerprint']):
 
1075
                        return
 
1076
            time.sleep(.1)
 
1077
        raise JujuResourceTimeout(
 
1078
            'Timeout waiting for a resource to be downloaded. '
 
1079
            'ResourceId: {} Service or Unit: {} Timeout: {}'.format(
 
1080
                resource_id, service_or_unit, timeout))
1465
1081
 
1466
1082
    def upgrade_charm(self, service, charm_path=None):
1467
1083
        args = (service,)
1499
1115
        self.juju('deployer', args, self.env.needs_sudo(), include_e=False)
1500
1116
 
1501
1117
    def _get_substrate_constraints(self):
1502
 
        if self.env.joyent:
 
1118
        if self.env.maas:
 
1119
            return 'mem=2G arch=amd64'
 
1120
        elif self.env.joyent:
1503
1121
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1504
1122
            return 'mem=2G cpu-cores=1'
1505
1123
        else:
1508
1126
    def quickstart(self, bundle_template, upload_tools=False):
1509
1127
        """quickstart, using sudo if necessary."""
1510
1128
        bundle = self.format_bundle(bundle_template)
1511
 
        constraints = 'mem=2G'
 
1129
        if self.env.maas:
 
1130
            constraints = 'mem=2G arch=amd64'
 
1131
        else:
 
1132
            constraints = 'mem=2G'
1512
1133
        args = ('--constraints', constraints)
1513
1134
        if upload_tools:
1514
1135
            args = ('--upload-tools',) + args
1527
1148
        :param start: If supplied, the time to count from when determining
1528
1149
            timeout.
1529
1150
        """
1530
 
        with self.check_timeouts():
1531
 
            with self.ignore_soft_deadline():
1532
 
                yield self.get_status()
1533
 
                for remaining in until_timeout(timeout, start=start):
1534
 
                    yield self.get_status()
 
1151
        yield self.get_status()
 
1152
        for remaining in until_timeout(timeout, start=start):
 
1153
            yield self.get_status()
1535
1154
 
1536
1155
    def _wait_for_status(self, reporter, translate, exc_type=StatusNotMet,
1537
1156
                         timeout=1200, start=None):
1549
1168
        """
1550
1169
        status = None
1551
1170
        try:
1552
 
            with self.check_timeouts():
1553
 
                with self.ignore_soft_deadline():
1554
 
                    for _ in chain([None],
1555
 
                                   until_timeout(timeout, start=start)):
1556
 
                        try:
1557
 
                            status = self.get_status()
1558
 
                        except CannotConnectEnv:
1559
 
                            log.info(
1560
 
                                'Suppressing "Unable to connect to'
1561
 
                                ' environment"')
1562
 
                            continue
1563
 
                        states = translate(status)
1564
 
                        if states is None:
1565
 
                            break
1566
 
                        reporter.update(states)
1567
 
                    else:
1568
 
                        if status is not None:
1569
 
                            log.error(status.status_text)
1570
 
                        raise exc_type(self.env.environment, status)
 
1171
            for _ in chain([None], until_timeout(timeout, start=start)):
 
1172
                try:
 
1173
                    status = self.get_status()
 
1174
                except CannotConnectEnv:
 
1175
                    log.info('Suppressing "Unable to connect to environment"')
 
1176
                    continue
 
1177
                states = translate(status)
 
1178
                if states is None:
 
1179
                    break
 
1180
                reporter.update(states)
 
1181
            else:
 
1182
                if status is not None:
 
1183
                    log.error(status.status_text)
 
1184
                raise exc_type(self.env.environment, status)
1571
1185
        finally:
1572
1186
            reporter.finish()
1573
1187
        return status
1615
1229
        self.controller_juju('list-models', ())
1616
1230
 
1617
1231
    def get_models(self):
1618
 
        """return a models dict with a 'models': [] key-value pair.
1619
 
 
1620
 
        The server has 120 seconds to respond because this method is called
1621
 
        often when tearing down a controller-less deployment.
1622
 
        """
 
1232
        """return a models dict with a 'models': [] key-value pair."""
1623
1233
        output = self.get_juju_output(
1624
1234
            'list-models', '-c', self.env.controller.name, '--format', 'yaml',
1625
 
            include_e=False, timeout=120)
 
1235
            include_e=False)
1626
1236
        models = yaml_loads(output)
1627
1237
        return models
1628
1238
 
1641
1251
        for model in models:
1642
1252
            yield self._acquire_model_client(model['name'])
1643
1253
 
1644
 
    def get_controller_model_name(self):
1645
 
        """Return the name of the 'controller' model.
 
1254
    def get_admin_model_name(self):
 
1255
        """Return the name of the 'admin' model.
1646
1256
 
1647
 
        Return the name of the environment when an 'controller' model does
 
1257
        Return the name of the environment when an 'admin' model does
1648
1258
        not exist.
1649
1259
        """
1650
1260
        return 'controller'
1660
1270
            env = self.env.clone(model_name=name)
1661
1271
            return self.clone(env=env)
1662
1272
 
1663
 
    def get_model_uuid(self):
1664
 
        name = self.env.environment
1665
 
        model = self._cmd_model(True, False)
1666
 
        output_yaml = self.get_juju_output(
1667
 
            'show-model', '--format', 'yaml', model, include_e=False)
1668
 
        output = yaml.safe_load(output_yaml)
1669
 
        return output[name]['model-uuid']
1670
 
 
1671
 
    def get_controller_uuid(self):
1672
 
        name = self.env.controller.name
1673
 
        output_yaml = self.get_juju_output(
1674
 
            'show-controller', '--format', 'yaml', include_e=False)
1675
 
        output = yaml.safe_load(output_yaml)
1676
 
        return output[name]['details']['uuid']
1677
 
 
1678
 
    def get_controller_model_uuid(self):
1679
 
        output_yaml = self.get_juju_output(
1680
 
            'show-model', 'controller', '--format', 'yaml', include_e=False)
1681
 
        output = yaml.safe_load(output_yaml)
1682
 
        return output['controller']['model-uuid']
1683
 
 
1684
 
    def get_controller_client(self):
1685
 
        """Return a client for the controller model.  May return self.
 
1273
    def get_admin_client(self):
 
1274
        """Return a client for the admin model.  May return self.
1686
1275
 
1687
1276
        This may be inaccurate for models created using add_model
1688
1277
        rather than bootstrap.
1689
1278
        """
1690
 
        return self._acquire_model_client(self.get_controller_model_name())
 
1279
        return self._acquire_model_client(self.get_admin_model_name())
1691
1280
 
1692
1281
    def list_controllers(self):
1693
1282
        """List the controllers."""
1738
1327
        desired_state = 'has-vote'
1739
1328
        reporter = GroupReporter(sys.stdout, desired_state)
1740
1329
        try:
1741
 
            with self.check_timeouts():
1742
 
                with self.ignore_soft_deadline():
1743
 
                    for remaining in until_timeout(timeout):
1744
 
                        status = self.get_status(controller=True)
1745
 
                        status.check_agents_started()
1746
 
                        states = {}
1747
 
                        for machine, info in status.iter_machines():
1748
 
                            status = self.get_controller_member_status(info)
1749
 
                            if status is None:
1750
 
                                continue
1751
 
                            states.setdefault(status, []).append(machine)
1752
 
                        if states.keys() == [desired_state]:
1753
 
                            if len(states.get(desired_state, [])) >= 3:
1754
 
                                break
1755
 
                        reporter.update(states)
1756
 
                    else:
1757
 
                        raise Exception('Timed out waiting for voting to be'
1758
 
                                        ' enabled.')
 
1330
            for remaining in until_timeout(timeout):
 
1331
                status = self.get_status(admin=True)
 
1332
                states = {}
 
1333
                for machine, info in status.iter_machines():
 
1334
                    status = self.get_controller_member_status(info)
 
1335
                    if status is None:
 
1336
                        continue
 
1337
                    states.setdefault(status, []).append(machine)
 
1338
                if states.keys() == [desired_state]:
 
1339
                    if len(states.get(desired_state, [])) >= 3:
 
1340
                        break
 
1341
                reporter.update(states)
 
1342
            else:
 
1343
                raise Exception('Timed out waiting for voting to be enabled.')
1759
1344
        finally:
1760
1345
            reporter.finish()
1761
1346
        # XXX sinzui 2014-12-04: bug 1399277 happens because
1770
1355
        :param service_count: The number of services for which to wait.
1771
1356
        :param timeout: The number of seconds to wait.
1772
1357
        """
1773
 
        with self.check_timeouts():
1774
 
            with self.ignore_soft_deadline():
1775
 
                for remaining in until_timeout(timeout):
1776
 
                    status = self.get_status()
1777
 
                    if status.get_service_count() >= service_count:
1778
 
                        return
1779
 
                else:
1780
 
                    raise Exception('Timed out waiting for services to start.')
 
1358
        for remaining in until_timeout(timeout):
 
1359
            status = self.get_status()
 
1360
            if status.get_service_count() >= service_count:
 
1361
                return
 
1362
        else:
 
1363
            raise Exception('Timed out waiting for services to start.')
1781
1364
 
1782
1365
    def wait_for_workloads(self, timeout=600, start=None):
1783
1366
        """Wait until all unit workloads are in a ready state."""
1862
1445
        args = ()
1863
1446
        if force_version:
1864
1447
            version = self.get_matching_agent_version(no_build=True)
1865
 
            args += ('--agent-version', version)
1866
 
        self._upgrade_juju(args)
1867
 
 
1868
 
    def _upgrade_juju(self, args):
 
1448
            args += ('--version', version)
 
1449
        if self.env.local:
 
1450
            args += ('--upload-tools',)
1869
1451
        self.juju('upgrade-juju', args)
1870
1452
 
1871
1453
    def upgrade_mongo(self):
1889
1471
        return backup_file_path
1890
1472
 
1891
1473
    def restore_backup(self, backup_file):
1892
 
        self.juju(
1893
 
            'restore-backup',
1894
 
            ('-b', '--constraints', 'mem=2G', '--file', backup_file))
 
1474
        return self.get_juju_output('restore-backup', '-b', '--constraints',
 
1475
                                    'mem=2G', '--file', backup_file)
1895
1476
 
1896
1477
    def restore_backup_async(self, backup_file):
1897
1478
        return self.juju_async('restore-backup', ('-b', '--constraints',
1977
1558
                return command_parts[-1]
1978
1559
        raise AssertionError('Juju register command not found in output')
1979
1560
 
1980
 
    def add_user(self, username):
1981
 
        """Adds provided user and return register command arguments.
1982
 
 
1983
 
        :return: Registration token provided by the add-user command.
1984
 
        """
1985
 
        output = self.get_juju_output(
1986
 
            'add-user', username, '-c', self.env.controller.name,
1987
 
            include_e=False)
1988
 
        return self._get_register_command(output)
1989
 
 
1990
 
    def add_user_perms(self, username, models=None, permissions='login'):
1991
 
        """Adds provided user and return register command arguments.
1992
 
 
1993
 
        :return: Registration token provided by the add-user command.
1994
 
        """
1995
 
        output = self.add_user(username)
1996
 
        self.grant(username, permissions, models)
1997
 
        return output
1998
 
 
1999
 
    def revoke(self, username, models=None, permissions='read'):
2000
 
        if models is None:
2001
 
            models = self.env.environment
2002
 
 
2003
 
        args = (username, permissions, models)
2004
 
 
2005
 
        self.controller_juju('revoke', args)
2006
 
 
2007
 
    def add_storage(self, unit, storage_type, amount="1"):
2008
 
        """Add storage instances to service.
2009
 
 
2010
 
        Only type 'disk' is able to add instances.
2011
 
        """
2012
 
        self.juju('add-storage', (unit, storage_type + "=" + amount))
2013
 
 
2014
 
    def list_storage(self):
2015
 
        """Return the storage list."""
2016
 
        return self.get_juju_output('list-storage', '--format', 'json')
2017
 
 
2018
 
    def list_storage_pool(self):
2019
 
        """Return the list of storage pool."""
2020
 
        return self.get_juju_output('list-storage-pools', '--format', 'json')
2021
 
 
2022
 
    def create_storage_pool(self, name, provider, size):
2023
 
        """Create storage pool."""
2024
 
        self.juju('create-storage-pool',
2025
 
                  (name, provider,
2026
 
                   'size={}'.format(size)))
2027
 
 
2028
 
    def disable_user(self, user_name):
2029
 
        """Disable an user"""
2030
 
        self.controller_juju('disable-user', (user_name,))
2031
 
 
2032
 
    def enable_user(self, user_name):
2033
 
        """Enable an user"""
2034
 
        self.controller_juju('enable-user', (user_name,))
2035
 
 
2036
 
    def logout(self):
2037
 
        """Logout an user"""
2038
 
        self.controller_juju('logout', ())
2039
 
        self.env.user_name = ''
2040
 
 
2041
 
    def register_user(self, user, juju_home, controller_name=None):
2042
 
        """Register `user` for the `client` return the cloned client used."""
2043
 
        username = user.name
2044
 
        if controller_name is None:
2045
 
            controller_name = '{}_controller'.format(username)
2046
 
 
2047
 
        model = self.env.environment
2048
 
        token = self.add_user_perms(username, models=model,
2049
 
                                    permissions=user.permissions)
2050
 
        user_client = self.create_cloned_environment(juju_home,
2051
 
                                                     controller_name,
2052
 
                                                     username)
2053
 
 
2054
 
        try:
2055
 
            child = user_client.expect('register', (token), include_e=False)
2056
 
            child.expect('(?i)name')
2057
 
            child.sendline(controller_name)
2058
 
            child.expect('(?i)password')
2059
 
            child.sendline(username + '_password')
2060
 
            child.expect('(?i)password')
2061
 
            child.sendline(username + '_password')
2062
 
            child.expect(pexpect.EOF)
2063
 
            if child.isalive():
2064
 
                raise Exception(
2065
 
                    'Registering user failed: pexpect session still alive')
2066
 
        except pexpect.TIMEOUT:
2067
 
            raise Exception(
2068
 
                'Registering user failed: pexpect session timed out')
2069
 
        user_client.env.user_name = username
2070
 
        return user_client
2071
 
 
2072
 
    def remove_user(self, username):
2073
 
        self.juju('remove-user', (username, '-y'), include_e=False)
2074
 
 
2075
 
    def create_cloned_environment(
2076
 
            self, cloned_juju_home, controller_name, user_name=None):
2077
 
        """Create a cloned environment.
2078
 
 
2079
 
        If `user_name` is passed ensures that the cloned environment is updated
2080
 
        to match.
2081
 
 
2082
 
        """
2083
 
        user_client = self.clone(env=self.env.clone())
2084
 
        user_client.env.juju_home = cloned_juju_home
2085
 
        if user_name is not None and user_name != self.env.user_name:
2086
 
            user_client.env.user_name = user_name
2087
 
            user_client.env.environment = qualified_model_name(
2088
 
                user_client.env.environment, self.env.user_name)
2089
 
        # New user names the controller.
2090
 
        user_client.env.controller = Controller(controller_name)
2091
 
        return user_client
2092
 
 
2093
 
    def grant(self, user_name, permission, model=None):
2094
 
        """Grant the user with model or controller permission."""
2095
 
        if permission in self.controller_permissions:
2096
 
            self.juju(
2097
 
                'grant',
2098
 
                (user_name, permission, '-c', self.env.controller.name),
2099
 
                include_e=False)
2100
 
        elif permission in self.model_permissions:
2101
 
            if model is None:
2102
 
                model = self.model_name
2103
 
            self.juju(
2104
 
                'grant',
2105
 
                (user_name, permission, model, '-c', self.env.controller.name),
2106
 
                include_e=False)
2107
 
        else:
2108
 
            raise ValueError('Unknown permission {}'.format(permission))
2109
 
 
2110
 
    def list_clouds(self, format='json'):
2111
 
        """List all the available clouds."""
2112
 
        return self.get_juju_output('list-clouds', '--format',
2113
 
                                    format, include_e=False)
2114
 
 
2115
 
    def show_controller(self, format='json'):
2116
 
        """Show controller's status."""
2117
 
        return self.get_juju_output('show-controller', '--format',
2118
 
                                    format, include_e=False)
2119
 
 
2120
 
    def ssh_keys(self, full=False):
2121
 
        """Give the ssh keys registered for the current model."""
2122
 
        args = []
2123
 
        if full:
2124
 
            args.append('--full')
2125
 
        return self.get_juju_output('ssh-keys', *args)
2126
 
 
2127
 
    def add_ssh_key(self, *keys):
2128
 
        """Add one or more ssh keys to the current model."""
2129
 
        return self.get_juju_output('add-ssh-key', *keys, merge_stderr=True)
2130
 
 
2131
 
    def remove_ssh_key(self, *keys):
2132
 
        """Remove one or more ssh keys from the current model."""
2133
 
        return self.get_juju_output('remove-ssh-key', *keys, merge_stderr=True)
2134
 
 
2135
 
    def import_ssh_key(self, *keys):
2136
 
        """Import ssh keys from one or more identities to the current model."""
2137
 
        return self.get_juju_output('import-ssh-key', *keys, merge_stderr=True)
2138
 
 
2139
 
    def list_disabled_commands(self):
2140
 
        """List all the commands disabled on the model."""
2141
 
        raw = self.get_juju_output('list-disabled-commands',
2142
 
                                   '--format', 'yaml')
2143
 
        return yaml.safe_load(raw)
2144
 
 
2145
 
    def disable_command(self, args):
2146
 
        """Disable a command set."""
2147
 
        return self.juju('disable-command', args)
2148
 
 
2149
 
    def enable_command(self, args):
2150
 
        """Enable a command set."""
2151
 
        return self.juju('enable-command', args)
2152
 
 
2153
 
    def sync_tools(self, local_dir=None):
2154
 
        """Copy tools into a local directory or model."""
2155
 
        if local_dir is None:
2156
 
            return self.juju('sync-tools', ())
2157
 
        else:
2158
 
            return self.juju('sync-tools', ('--local-dir', local_dir),
2159
 
                             include_e=False)
2160
 
 
2161
 
 
2162
 
class EnvJujuClient2B9(EnvJujuClient):
2163
 
 
2164
 
    def update_user_name(self):
2165
 
        return
2166
 
 
2167
 
    def create_cloned_environment(
2168
 
            self, cloned_juju_home, controller_name, user_name=None):
2169
 
        """Create a cloned environment.
2170
 
 
2171
 
        `user_name` is unused in this version of beta.
2172
 
        """
2173
 
        user_client = self.clone(env=self.env.clone())
2174
 
        user_client.env.juju_home = cloned_juju_home
2175
 
        # New user names the controller.
2176
 
        user_client.env.controller = Controller(controller_name)
2177
 
        return user_client
2178
 
 
2179
 
    def get_model_uuid(self):
2180
 
        name = self.env.environment
2181
 
        output_yaml = self.get_juju_output('show-model', '--format', 'yaml')
2182
 
        output = yaml.safe_load(output_yaml)
2183
 
        return output[name]['model-uuid']
2184
 
 
2185
 
    def add_user_perms(self, username, models=None, permissions='read'):
 
1561
    def add_user(self, username, models=None, permissions='read'):
2186
1562
        """Adds provided user and return register command arguments.
2187
1563
 
2188
1564
        :return: Registration token provided by the add-user command.
2197
1573
        output = self.get_juju_output('add-user', *args, include_e=False)
2198
1574
        return self._get_register_command(output)
2199
1575
 
2200
 
    def grant(self, user_name, permission, model=None):
2201
 
        """Grant the user with a model."""
2202
 
        if model is None:
2203
 
            model = self.model_name
2204
 
        self.juju('grant', (user_name, model, '--acl', permission),
2205
 
                  include_e=False)
2206
 
 
2207
1576
    def revoke(self, username, models=None, permissions='read'):
2208
1577
        if models is None:
2209
1578
            models = self.env.environment
2212
1581
 
2213
1582
        self.controller_juju('revoke', args)
2214
1583
 
2215
 
    def set_config(self, service, options):
2216
 
        option_strings = self._dict_as_option_strings(options)
2217
 
        self.juju('set-config', (service,) + option_strings)
2218
 
 
2219
 
    def get_config(self, service):
2220
 
        return yaml_loads(self.get_juju_output('get-config', service))
2221
 
 
2222
 
    def get_model_config(self):
2223
 
        """Return the value of the environment's configured option."""
2224
 
        return yaml.safe_load(
2225
 
            self.get_juju_output('get-model-config', '--format', 'yaml'))
2226
 
 
2227
 
    def get_env_option(self, option):
2228
 
        """Return the value of the environment's configured option."""
2229
 
        return self.get_juju_output('get-model-config', option)
2230
 
 
2231
 
    def set_env_option(self, option, value):
2232
 
        """Set the value of the option in the environment."""
2233
 
        option_value = "%s=%s" % (option, value)
2234
 
        return self.juju('set-model-config', (option_value,))
2235
 
 
2236
 
    def unset_env_option(self, option):
2237
 
        """Unset the value of the option in the environment."""
2238
 
        return self.juju('unset-model-config', (option,))
2239
 
 
2240
 
    def list_disabled_commands(self):
2241
 
        """List all the commands disabled on the model."""
2242
 
        raw = self.get_juju_output('block list', '--format', 'yaml')
2243
 
        return yaml.safe_load(raw)
2244
 
 
2245
 
    def disable_command(self, args):
2246
 
        """Disable a command set."""
2247
 
        return self.juju('block', args)
2248
 
 
2249
 
    def enable_command(self, args):
2250
 
        """Enable a command set."""
2251
 
        return self.juju('unblock', args)
2252
 
 
2253
 
 
2254
 
class EnvJujuClient2B8(EnvJujuClient2B9):
 
1584
 
 
1585
class EnvJujuClient2B8(EnvJujuClient):
2255
1586
 
2256
1587
    status_class = ServiceStatus
2257
1588
 
2284
1615
 
2285
1616
class EnvJujuClient2B7(EnvJujuClient2B8):
2286
1617
 
2287
 
    def get_controller_model_name(self):
2288
 
        """Return the name of the 'controller' model.
 
1618
    def get_admin_model_name(self):
 
1619
        """Return the name of the 'admin' model.
2289
1620
 
2290
 
        Return the name of the environment when an 'controller' model does
 
1621
        Return the name of the environment when an 'admin' model does
2291
1622
        not exist.
2292
1623
        """
2293
1624
        return 'admin'
2302
1633
 
2303
1634
class EnvJujuClient2B2(EnvJujuClient2B3):
2304
1635
 
2305
 
    def get_bootstrap_args(
2306
 
            self, upload_tools, config_filename, bootstrap_series=None,
2307
 
            credential=None, auto_upgrade=False, metadata_source=None,
2308
 
            to=None, agent_version=None):
 
1636
    def get_bootstrap_args(self, upload_tools, config_filename,
 
1637
                           bootstrap_series=None):
2309
1638
        """Return the bootstrap arguments for the substrate."""
2310
 
        err_fmt = 'EnvJujuClient2B2 does not support bootstrap argument {}'
2311
 
        if auto_upgrade:
2312
 
            raise ValueError(err_fmt.format('auto_upgrade'))
2313
 
        if metadata_source is not None:
2314
 
            raise ValueError(err_fmt.format('metadata_source'))
2315
 
        if to is not None:
2316
 
            raise ValueError(err_fmt.format('to'))
2317
 
        if agent_version is not None:
2318
 
            raise ValueError(err_fmt.format('agent_version'))
2319
 
        if self.env.joyent:
 
1639
        if self.env.maas:
 
1640
            constraints = 'mem=2G arch=amd64'
 
1641
        elif self.env.joyent:
2320
1642
            # Only accept kvm packages by requiring >1 cpu core, see lp:1446264
2321
1643
            constraints = 'mem=2G cpu-cores=1'
2322
1644
        else:
2332
1654
 
2333
1655
        if bootstrap_series is not None:
2334
1656
            args.extend(['--bootstrap-series', bootstrap_series])
2335
 
 
2336
 
        if credential is not None:
2337
 
            args.extend(['--credential', credential])
2338
 
 
2339
1657
        return tuple(args)
2340
1658
 
2341
 
    def get_controller_client(self):
2342
 
        """Return a client for the controller model.  May return self."""
 
1659
    def get_admin_client(self):
 
1660
        """Return a client for the admin model.  May return self."""
2343
1661
        return self
2344
1662
 
2345
 
    def get_controller_model_name(self):
2346
 
        """Return the name of the 'controller' model.
 
1663
    def get_admin_model_name(self):
 
1664
        """Return the name of the 'admin' model.
2347
1665
 
2348
 
        Return the name of the environment when an 'controller' model does
 
1666
        Return the name of the environment when an 'admin' model does
2349
1667
        not exist.
2350
1668
        """
2351
1669
        models = self.get_models()
2352
1670
        # The dict can be empty because 1.x does not support the models.
2353
1671
        # This is an ambiguous case for the jes feature flag which supports
2354
1672
        # multiple models, but none is named 'admin' by default. Since the
2355
 
        # jes case also uses '-e' for models, the env is the controller model.
 
1673
        # jes case also uses '-e' for models, the env is the admin model.
2356
1674
        for model in models.get('models', []):
2357
1675
            if 'admin' in model['name']:
2358
1676
                return 'admin'
2364
1682
 
2365
1683
    default_backend = Juju2A2Backend
2366
1684
 
2367
 
    config_class = SimpleEnvironment
2368
 
 
2369
1685
    @classmethod
2370
1686
    def _get_env(cls, env):
2371
1687
        if isinstance(env, JujuData):
2372
 
            raise IncompatibleConfigClass(
 
1688
            raise ValueError(
2373
1689
                'JujuData cannot be used with {}'.format(cls.__name__))
2374
1690
        return env
2375
1691
 
2388
1704
            log.info('Waiting for bootstrap of {}.'.format(
2389
1705
                self.env.environment))
2390
1706
 
2391
 
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2392
 
                           credential=None):
 
1707
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None):
2393
1708
        """Return the bootstrap arguments for the substrate."""
2394
 
        if credential is not None:
2395
 
            raise ValueError(
2396
 
                '--credential is not supported by this juju version.')
2397
1709
        constraints = self._get_substrate_constraints()
2398
1710
        args = ('--constraints', constraints,
2399
1711
                '--agent-version', self.get_matching_agent_version())
2404
1716
        return args
2405
1717
 
2406
1718
    def deploy(self, charm, repository=None, to=None, series=None,
2407
 
               service=None, force=False, storage=None, constraints=None):
 
1719
               service=None, force=False):
2408
1720
        args = [charm]
2409
1721
        if repository is not None:
2410
1722
            args.extend(['--repository', repository])
2412
1724
            args.extend(['--to', to])
2413
1725
        if service is not None:
2414
1726
            args.extend([service])
2415
 
        if storage is not None:
2416
 
            args.extend(['--storage', storage])
2417
 
        if constraints is not None:
2418
 
            args.extend(['--constraints', constraints])
2419
1727
        return self.juju('deploy', tuple(args))
2420
1728
 
2421
1729
 
2472
1780
        """List the models registered with the current controller."""
2473
1781
        log.info('The model is environment {}'.format(self.env.environment))
2474
1782
 
2475
 
    def list_clouds(self, format='json'):
2476
 
        """List all the available clouds."""
2477
 
        return {}
2478
 
 
2479
 
    def show_controller(self, format='json'):
2480
 
        """Show controller's status."""
2481
 
        return {}
2482
 
 
2483
1783
    def get_models(self):
2484
1784
        """return a models dict with a 'models': [] key-value pair."""
2485
1785
        return {}
2584
1884
 
2585
1885
    supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE])
2586
1886
 
2587
 
    agent_metadata_url = 'tools-metadata-url'
2588
 
 
2589
 
    def _cmd_model(self, include_e, controller):
2590
 
        if controller:
2591
 
            return self.get_controller_model_name()
 
1887
    def _cmd_model(self, include_e, admin):
 
1888
        if admin:
 
1889
            return self.get_admin_model_name()
2592
1890
        elif self.env is None or not include_e:
2593
1891
            return None
2594
1892
        else:
2595
 
            return unqualified_model_name(self.model_name)
 
1893
            return self.model_name
2596
1894
 
2597
 
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2598
 
                           credential=None):
 
1895
    def get_bootstrap_args(self, upload_tools, bootstrap_series=None):
2599
1896
        """Return the bootstrap arguments for the substrate."""
2600
 
        if credential is not None:
2601
 
            raise ValueError(
2602
 
                '--credential is not supported by this juju version.')
2603
1897
        constraints = self._get_substrate_constraints()
2604
1898
        args = ('--constraints', constraints)
2605
1899
        if upload_tools:
2630
1924
                    return cmd
2631
1925
        raise JESNotSupported()
2632
1926
 
2633
 
    def upgrade_juju(self, force_version=True):
2634
 
        args = ()
2635
 
        if force_version:
2636
 
            version = self.get_matching_agent_version(no_build=True)
2637
 
            args += ('--version', version)
2638
 
        if self.env.local:
2639
 
            args += ('--upload-tools',)
2640
 
        self._upgrade_juju(args)
2641
 
 
2642
1927
    def make_model_config(self):
2643
1928
        config_dict = make_safe_config(self)
2644
1929
        # Strip unneeded variables.
2666
1951
            'destroy-environment',
2667
1952
            (self.env.environment,) + force_arg + ('-y',),
2668
1953
            self.env.needs_sudo(), check=False, include_e=False,
2669
 
            timeout=get_teardown_timeout(self))
 
1954
            timeout=timedelta(minutes=10).total_seconds())
2670
1955
        if delete_jenv:
2671
1956
            jenv_path = get_jenv_path(self.env.juju_home, self.env.environment)
2672
1957
            ensure_deleted(jenv_path)
2719
2004
    def upgrade_mongo(self):
2720
2005
        raise UpgradeMongoNotSupported()
2721
2006
 
2722
 
    def add_storage(self, unit, storage_type, amount="1"):
2723
 
        """Add storage instances to service.
2724
 
 
2725
 
        Only type 'disk' is able to add instances.
2726
 
        """
2727
 
        self.juju('storage add', (unit, storage_type + "=" + amount))
2728
 
 
2729
 
    def list_storage(self):
2730
 
        """Return the storage list."""
2731
 
        return self.get_juju_output('storage list', '--format', 'json')
2732
 
 
2733
 
    def list_storage_pool(self):
2734
 
        """Return the list of storage pool."""
2735
 
        return self.get_juju_output('storage pool list', '--format', 'json')
2736
 
 
2737
 
    def create_storage_pool(self, name, provider, size):
2738
 
        """Create storage pool."""
2739
 
        self.juju('storage pool create',
2740
 
                  (name, provider,
2741
 
                   'size={}'.format(size)))
2742
 
 
2743
 
    def ssh_keys(self, full=False):
2744
 
        """Give the ssh keys registered for the current model."""
2745
 
        args = []
2746
 
        if full:
2747
 
            args.append('--full')
2748
 
        return self.get_juju_output('authorized-keys list', *args)
2749
 
 
2750
 
    def add_ssh_key(self, *keys):
2751
 
        """Add one or more ssh keys to the current model."""
2752
 
        return self.get_juju_output('authorized-keys add', *keys,
2753
 
                                    merge_stderr=True)
2754
 
 
2755
 
    def remove_ssh_key(self, *keys):
2756
 
        """Remove one or more ssh keys from the current model."""
2757
 
        return self.get_juju_output('authorized-keys delete', *keys,
2758
 
                                    merge_stderr=True)
2759
 
 
2760
 
    def import_ssh_key(self, *keys):
2761
 
        """Import ssh keys from one or more identities to the current model."""
2762
 
        return self.get_juju_output('authorized-keys import', *keys,
2763
 
                                    merge_stderr=True)
2764
 
 
2765
2007
 
2766
2008
class EnvJujuClient22(EnvJujuClient1X):
2767
2009
 
2903
2145
 
2904
2146
 
2905
2147
def dump_environments_yaml(juju_home, config):
2906
 
    """Dump yaml data to the environment file.
2907
 
 
2908
 
    :param juju_home: Path to the JUJU_HOME directory.
2909
 
    :param config: Dictionary repersenting yaml data to dump."""
2910
2148
    environments_path = get_environments_path(juju_home)
2911
2149
    with open(environments_path, 'w') as config_file:
2912
2150
        yaml.safe_dump(config, config_file)
2955
2193
    config['test-mode'] = True
2956
2194
    # Explicitly set 'name', which Juju implicitly sets to env.environment to
2957
2195
    # ensure MAASAccount knows what the name will be.
2958
 
    config['name'] = unqualified_model_name(client.env.environment)
 
2196
    config['name'] = client.env.environment
2959
2197
    if config['type'] == 'local':
2960
2198
        config.setdefault('root-dir', get_local_root(client.env.juju_home,
2961
2199
                          client.env))
3044
2282
 
3045
2283
 
3046
2284
class Controller:
3047
 
    """Represents the controller for a model or models."""
3048
2285
 
3049
2286
    def __init__(self, name):
3050
2287
        self.name = name
3051
2288
 
3052
2289
 
 
2290
class SimpleEnvironment:
 
2291
 
 
2292
    def __init__(self, environment, config=None, juju_home=None,
 
2293
                 controller=None):
 
2294
        if controller is None:
 
2295
            controller = Controller(environment)
 
2296
        self.controller = controller
 
2297
        self.environment = environment
 
2298
        self.config = config
 
2299
        self.juju_home = juju_home
 
2300
        if self.config is not None:
 
2301
            self.local = bool(self.config.get('type') == 'local')
 
2302
            self.kvm = (
 
2303
                self.local and bool(self.config.get('container') == 'kvm'))
 
2304
            self.maas = bool(self.config.get('type') == 'maas')
 
2305
            self.joyent = bool(self.config.get('type') == 'joyent')
 
2306
        else:
 
2307
            self.local = False
 
2308
            self.kvm = False
 
2309
            self.maas = False
 
2310
            self.joyent = False
 
2311
 
 
2312
    def clone(self, model_name=None):
 
2313
        config = deepcopy(self.config)
 
2314
        if model_name is None:
 
2315
            model_name = self.environment
 
2316
        else:
 
2317
            config['name'] = model_name
 
2318
        result = self.__class__(model_name, config, self.juju_home,
 
2319
                                self.controller)
 
2320
        result.local = self.local
 
2321
        result.kvm = self.kvm
 
2322
        result.maas = self.maas
 
2323
        result.joyent = self.joyent
 
2324
        return result
 
2325
 
 
2326
    def __eq__(self, other):
 
2327
        if type(self) != type(other):
 
2328
            return False
 
2329
        if self.environment != other.environment:
 
2330
            return False
 
2331
        if self.config != other.config:
 
2332
            return False
 
2333
        if self.local != other.local:
 
2334
            return False
 
2335
        if self.maas != other.maas:
 
2336
            return False
 
2337
        return True
 
2338
 
 
2339
    def __ne__(self, other):
 
2340
        return not self == other
 
2341
 
 
2342
    def set_model_name(self, model_name, set_controller=True):
 
2343
        if set_controller:
 
2344
            self.controller.name = model_name
 
2345
        self.environment = model_name
 
2346
        self.config['name'] = model_name
 
2347
 
 
2348
    @classmethod
 
2349
    def from_config(cls, name):
 
2350
        return cls._from_config(name)
 
2351
 
 
2352
    @classmethod
 
2353
    def _from_config(cls, name):
 
2354
        config, selected = get_selected_environment(name)
 
2355
        if name is None:
 
2356
            name = selected
 
2357
        return cls(name, config)
 
2358
 
 
2359
    def needs_sudo(self):
 
2360
        return self.local
 
2361
 
 
2362
    @contextmanager
 
2363
    def make_jes_home(self, juju_home, dir_name, new_config):
 
2364
        home_path = jes_home_path(juju_home, dir_name)
 
2365
        if os.path.exists(home_path):
 
2366
            rmtree(home_path)
 
2367
        os.makedirs(home_path)
 
2368
        self.dump_yaml(home_path, new_config)
 
2369
        yield home_path
 
2370
 
 
2371
    def dump_yaml(self, path, config):
 
2372
        dump_environments_yaml(path, config)
 
2373
 
 
2374
 
 
2375
class JujuData(SimpleEnvironment):
 
2376
 
 
2377
    def __init__(self, environment, config=None, juju_home=None,
 
2378
                 controller=None):
 
2379
        if juju_home is None:
 
2380
            juju_home = get_juju_home()
 
2381
        super(JujuData, self).__init__(environment, config, juju_home,
 
2382
                                       controller)
 
2383
        self.credentials = {}
 
2384
        self.clouds = {}
 
2385
 
 
2386
    def clone(self, model_name=None):
 
2387
        result = super(JujuData, self).clone(model_name)
 
2388
        result.credentials = deepcopy(self.credentials)
 
2389
        result.clouds = deepcopy(self.clouds)
 
2390
        return result
 
2391
 
 
2392
    @classmethod
 
2393
    def from_env(cls, env):
 
2394
        juju_data = cls(env.environment, env.config, env.juju_home)
 
2395
        juju_data.load_yaml()
 
2396
        return juju_data
 
2397
 
 
2398
    def load_yaml(self):
 
2399
        try:
 
2400
            with open(os.path.join(self.juju_home, 'credentials.yaml')) as f:
 
2401
                self.credentials = yaml.safe_load(f)
 
2402
        except IOError as e:
 
2403
            if e.errno != errno.ENOENT:
 
2404
                raise RuntimeError(
 
2405
                    'Failed to read credentials file: {}'.format(str(e)))
 
2406
            self.credentials = {}
 
2407
        try:
 
2408
            with open(os.path.join(self.juju_home, 'clouds.yaml')) as f:
 
2409
                self.clouds = yaml.safe_load(f)
 
2410
        except IOError as e:
 
2411
            if e.errno != errno.ENOENT:
 
2412
                raise RuntimeError(
 
2413
                    'Failed to read clouds file: {}'.format(str(e)))
 
2414
            # Default to an empty clouds file.
 
2415
            self.clouds = {'clouds': {}}
 
2416
 
 
2417
    @classmethod
 
2418
    def from_config(cls, name):
 
2419
        juju_data = cls._from_config(name)
 
2420
        juju_data.load_yaml()
 
2421
        return juju_data
 
2422
 
 
2423
    def dump_yaml(self, path, config):
 
2424
        """Dump the configuration files to the specified path.
 
2425
 
 
2426
        config is unused, but is accepted for compatibility with
 
2427
        SimpleEnvironment and make_jes_home().
 
2428
        """
 
2429
        with open(os.path.join(path, 'credentials.yaml'), 'w') as f:
 
2430
            yaml.safe_dump(self.credentials, f)
 
2431
        with open(os.path.join(path, 'clouds.yaml'), 'w') as f:
 
2432
            yaml.safe_dump(self.clouds, f)
 
2433
 
 
2434
    def find_endpoint_cloud(self, cloud_type, endpoint):
 
2435
        for cloud, cloud_config in self.clouds['clouds'].items():
 
2436
            if cloud_config['type'] != cloud_type:
 
2437
                continue
 
2438
            if cloud_config['endpoint'] == endpoint:
 
2439
                return cloud
 
2440
        raise LookupError('No such endpoint: {}'.format(endpoint))
 
2441
 
 
2442
    def get_cloud(self):
 
2443
        provider = self.config['type']
 
2444
        # Separate cloud recommended by: Juju Cloud / Credentials / BootStrap /
 
2445
        # Model CLI specification
 
2446
        if provider == 'ec2' and self.config['region'] == 'cn-north-1':
 
2447
            return 'aws-china'
 
2448
        if provider not in ('maas', 'openstack'):
 
2449
            return {
 
2450
                'ec2': 'aws',
 
2451
                'gce': 'google',
 
2452
            }.get(provider, provider)
 
2453
        if provider == 'maas':
 
2454
            endpoint = self.config['maas-server']
 
2455
        elif provider == 'openstack':
 
2456
            endpoint = self.config['auth-url']
 
2457
        return self.find_endpoint_cloud(provider, endpoint)
 
2458
 
 
2459
    def get_region(self):
 
2460
        provider = self.config['type']
 
2461
        if provider == 'azure':
 
2462
            if 'tenant-id' not in self.config:
 
2463
                return self.config['location'].replace(' ', '').lower()
 
2464
            return self.config['location']
 
2465
        elif provider == 'joyent':
 
2466
            matcher = re.compile('https://(.*).api.joyentcloud.com')
 
2467
            return matcher.match(self.config['sdc-url']).group(1)
 
2468
        elif provider == 'lxd':
 
2469
            return 'localhost'
 
2470
        elif provider == 'manual':
 
2471
            return self.config['bootstrap-host']
 
2472
        elif provider in ('maas', 'manual'):
 
2473
            return None
 
2474
        else:
 
2475
            return self.config['region']
 
2476
 
 
2477
 
3053
2478
class GroupReporter:
3054
2479
 
3055
2480
    def __init__(self, stream, expected):