207
151
_fmt = 'Workloads not ready in {env}.'
211
def temp_yaml_file(yaml_dict):
212
temp_file = NamedTemporaryFile(suffix='.yaml', delete=False)
215
yaml.safe_dump(yaml_dict, temp_file)
218
os.unlink(temp_file.name)
221
class SimpleEnvironment:
222
"""Represents a model in a JUJU_HOME directory for juju 1."""
224
def __init__(self, environment, config=None, juju_home=None,
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
239
self.juju_home = juju_home
240
if self.config is not None:
241
self.local = bool(self.config.get('type') == 'local')
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')
252
def clone(self, model_name=None):
253
config = deepcopy(self.config)
254
if model_name is None:
255
model_name = self.environment
257
config['name'] = unqualified_model_name(model_name)
258
result = self.__class__(model_name, config, self.juju_home,
260
result.local = self.local
261
result.kvm = self.kvm
262
result.maas = self.maas
263
result.joyent = self.joyent
266
def __eq__(self, other):
267
if type(self) != type(other):
269
if self.environment != other.environment:
271
if self.config != other.config:
273
if self.local != other.local:
275
if self.maas != other.maas:
279
def __ne__(self, other):
280
return not self == other
282
def set_model_name(self, model_name, set_controller=True):
284
self.controller.name = model_name
285
self.environment = model_name
286
self.config['name'] = unqualified_model_name(model_name)
289
def from_config(cls, name):
290
"""Create an environment from the configuation file.
292
:param name: Name of the environment to get the configuration from."""
293
return cls._from_config(name)
296
def _from_config(cls, name):
297
config, selected = get_selected_environment(name)
300
return cls(name, config)
302
def needs_sudo(self):
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):
310
os.makedirs(home_path)
311
self.dump_yaml(home_path, new_config)
314
def get_cloud_credentials(self):
315
"""Return the credentials for this model's cloud.
317
This implementation returns config variables in addition to
322
def dump_yaml(self, path, config):
323
dump_environments_yaml(path, config)
326
class JujuData(SimpleEnvironment):
327
"""Represents a model in a JUJU_DATA directory for juju 2."""
329
def __init__(self, environment, config=None, juju_home=None,
333
This extends SimpleEnvironment's constructor.
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.
342
if juju_home is None:
343
juju_home = get_juju_home()
344
super(JujuData, self).__init__(environment, config, juju_home,
346
self.credentials = {}
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)
356
def from_env(cls, env):
357
juju_data = cls(env.environment, env.config, env.juju_home)
358
juju_data.load_yaml()
363
with open(os.path.join(self.juju_home, 'credentials.yaml')) as f:
364
self.credentials = yaml.safe_load(f)
366
if e.errno != errno.ENOENT:
368
'Failed to read credentials file: {}'.format(str(e)))
369
self.credentials = {}
371
with open(os.path.join(self.juju_home, 'clouds.yaml')) as f:
372
self.clouds = yaml.safe_load(f)
374
if e.errno != errno.ENOENT:
376
'Failed to read clouds file: {}'.format(str(e)))
377
# Default to an empty clouds file.
378
self.clouds = {'clouds': {}}
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()
387
def dump_yaml(self, path, config):
388
"""Dump the configuration files to the specified path.
390
config is unused, but is accepted for compatibility with
391
SimpleEnvironment and make_jes_home().
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)
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:
402
if cloud_config['endpoint'] == endpoint:
404
raise LookupError('No such endpoint: {}'.format(endpoint))
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':
412
if provider not in ('maas', 'openstack'):
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)
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':
434
elif provider == 'manual':
435
return self.config['bootstrap-host']
436
elif provider in ('maas', 'manual'):
439
return self.config['region']
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()
451
def __init__(self, status, status_text):
453
self.status_text = status_text
456
def from_text(cls, text):
458
# Parsing as JSON is much faster than parsing as YAML, so try
459
# parsing as JSON first and fall back to YAML.
460
status_yaml = json.loads(text)
462
status_yaml = yaml_loads(text)
463
return cls(status_yaml, text)
465
def get_applications(self):
466
return self.status.get('applications', {})
468
def iter_machines(self, containers=False, machines=True):
469
for machine_name, machine in sorted(self.status['machines'].items()):
471
yield machine_name, machine
473
for contained, unit in machine.get('containers', {}).items():
474
yield contained, unit
476
def iter_new_machines(self, old_status):
477
for machine, data in self.iter_machines():
478
if machine in old_status.status['machines']:
482
def iter_units(self):
483
for service_name, service in sorted(self.get_applications().items()):
484
for unit_name, unit in sorted(service.get('units', {}).items()):
485
yield unit_name, unit
486
subordinates = unit.get('subordinates', ())
487
for sub_name in sorted(subordinates):
488
yield sub_name, subordinates[sub_name]
490
def agent_items(self):
491
for machine_name, machine in self.iter_machines(containers=True):
492
yield machine_name, machine
493
for unit_name, unit in self.iter_units():
494
yield unit_name, unit
496
def agent_states(self):
497
"""Map agent states to the units and machines in those states."""
498
states = defaultdict(list)
499
for item_name, item in self.agent_items():
500
states[coalesce_agent_status(item)].append(item_name)
503
def check_agents_started(self, environment_name=None):
504
"""Check whether all agents are in the 'started' state.
506
If not, return agent_states output. If so, return None.
507
If an error is encountered for an agent, raise ErroredUnit
509
bad_state_info = re.compile(
510
'(.*error|^(cannot set up groups|cannot run instance)).*')
511
for item_name, item in self.agent_items():
512
state_info = item.get('agent-state-info', '')
513
if bad_state_info.match(state_info):
514
raise ErroredUnit(item_name, state_info)
515
states = self.agent_states()
516
if set(states.keys()).issubset(AGENTS_READY):
518
for state, entries in states.items():
520
# sometimes the state may be hidden in juju status message
522
self.agent_items())[entries[0]].get('juju-status')
524
juju_status_msg = juju_status.get('message')
526
state = juju_status_msg
527
raise ErroredUnit(entries[0], state)
530
def get_service_count(self):
531
return len(self.get_applications())
533
def get_service_unit_count(self, service):
535
self.get_applications().get(service, {}).get('units', {}))
537
def get_agent_versions(self):
538
versions = defaultdict(set)
539
for item_name, item in self.agent_items():
540
if item.get('juju-status', None):
541
version = item['juju-status'].get('version', 'unknown')
542
versions[version].add(item_name)
544
versions[item.get('agent-version', 'unknown')].add(item_name)
547
def get_instance_id(self, machine_id):
548
return self.status['machines'][machine_id]['instance-id']
550
def get_unit(self, unit_name):
551
"""Return metadata about a unit."""
552
for service in sorted(self.get_applications().values()):
553
if unit_name in service.get('units', {}):
554
return service['units'][unit_name]
555
raise KeyError(unit_name)
557
def service_subordinate_units(self, service_name):
558
"""Return subordinate metadata for a service_name."""
559
services = self.get_applications()
560
if service_name in services:
561
for unit in sorted(services[service_name].get(
562
'units', {}).values()):
563
for sub_name, sub in unit.get('subordinates', {}).items():
566
def get_open_ports(self, unit_name):
567
"""List the open ports for the specified unit.
569
If no ports are listed for the unit, the empty list is returned.
571
return self.get_unit(unit_name).get('open-ports', [])
574
class ServiceStatus(Status):
576
def get_applications(self):
577
return self.status.get('services', {})
581
"""A Juju backend referring to a specific juju 2 binary.
583
Uses -m to specify models, uses JUJU_DATA to specify home directory.
586
def __init__(self, full_path, version, feature_flags, debug,
588
self._version = version
589
self._full_path = full_path
590
self.feature_flags = feature_flags
592
self._timeout_path = get_timeout_path()
593
self.juju_timings = {}
594
self.soft_deadline = soft_deadline
595
self._ignore_soft_deadline = False
598
return datetime.utcnow()
601
def _check_timeouts(self):
602
# If an exception occurred, we don't want to replace it with
603
# SoftDeadlineExceeded.
605
if self.soft_deadline is None or self._ignore_soft_deadline:
607
if self._now() > self.soft_deadline:
608
raise SoftDeadlineExceeded('Operation exceeded deadline.')
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
618
self._ignore_soft_deadline = old_val
620
def clone(self, full_path, version, debug, feature_flags):
622
version = self.version
623
if full_path is None:
624
full_path = self.full_path
627
result = self.__class__(full_path, version, feature_flags, debug,
637
return self._full_path
641
return os.path.basename(self._full_path)
643
def _get_attr_tuple(self):
644
return (self._version, self._full_path, self.feature_flags,
645
self.debug, self.juju_timings)
647
def __eq__(self, other):
648
if type(self) != type(other):
650
return self._get_attr_tuple() == other._get_attr_tuple()
652
def shell_environ(self, used_feature_flags, juju_home):
653
"""Generate a suitable shell environment.
655
Juju's directory must be in the PATH to support plugins.
657
env = dict(os.environ)
658
if self.full_path is not None:
659
env['PATH'] = '{}{}{}'.format(os.path.dirname(self.full_path),
660
os.pathsep, env['PATH'])
661
flags = self.feature_flags.intersection(used_feature_flags)
663
env[JUJU_DEV_FEATURE_FLAGS] = ','.join(sorted(flags))
664
env['JUJU_DATA'] = juju_home
667
def full_args(self, command, args, model, timeout):
668
if model is not None:
669
e_arg = ('-m', model)
675
prefix = get_timeout_prefix(timeout, self._timeout_path)
676
logging = '--debug' if self.debug else '--show-log'
678
# If args is a string, make it a tuple. This makes writing commands
679
# with one argument a bit nicer.
680
if isinstance(args, basestring):
682
# we split the command here so that the caller can control where the -m
683
# model flag goes. Everything in the command string is put before the
685
command = command.split()
686
return (prefix + (self.juju_name, logging,) + tuple(command) + e_arg +
689
def juju(self, command, args, used_feature_flags,
690
juju_home, model=None, check=True, timeout=None, extra_env=None):
691
"""Run a command under juju for the current environment."""
692
args = self.full_args(command, args, model, timeout)
693
log.info(' '.join(args))
694
env = self.shell_environ(used_feature_flags, juju_home)
695
if extra_env is not None:
696
env.update(extra_env)
698
call_func = subprocess.check_call
700
call_func = subprocess.call
701
start_time = time.time()
702
# Mutate os.environ instead of supplying env parameter so Windows can
704
with scoped_environ(env):
705
with self._check_timeouts():
706
rval = call_func(args)
707
self.juju_timings.setdefault(args, []).append(
708
(time.time() - start_time))
711
def expect(self, command, args, used_feature_flags, juju_home, model=None,
712
timeout=None, extra_env=None):
713
args = self.full_args(command, args, model, timeout)
714
log.info(' '.join(args))
715
env = self.shell_environ(used_feature_flags, juju_home)
716
if extra_env is not None:
717
env.update(extra_env)
718
# pexpect.spawn expects a string. This is better than trying to extract
719
# command + args from the returned tuple (as there could be an intial
720
# timing command tacked on).
721
command_string = ' '.join(quote(a) for a in args)
722
with scoped_environ(env):
723
return pexpect.spawn(command_string)
726
def juju_async(self, command, args, used_feature_flags,
727
juju_home, model=None, timeout=None):
728
full_args = self.full_args(command, args, model, timeout)
729
log.info(' '.join(args))
730
env = self.shell_environ(used_feature_flags, juju_home)
731
# Mutate os.environ instead of supplying env parameter so Windows can
733
with scoped_environ(env):
734
with self._check_timeouts():
735
proc = subprocess.Popen(full_args)
737
retcode = proc.wait()
739
raise subprocess.CalledProcessError(retcode, full_args)
741
def get_juju_output(self, command, args, used_feature_flags, juju_home,
742
model=None, timeout=None, user_name=None,
744
args = self.full_args(command, args, model, timeout)
745
env = self.shell_environ(used_feature_flags, juju_home)
747
# Mutate os.environ instead of supplying env parameter so
748
# Windows can search env['PATH']
749
with scoped_environ(env):
750
proc = subprocess.Popen(
751
args, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
752
stderr=subprocess.STDOUT if merge_stderr else subprocess.PIPE)
753
with self._check_timeouts():
754
sub_output, sub_error = proc.communicate()
755
log.debug(sub_output)
756
if proc.returncode != 0:
758
e = subprocess.CalledProcessError(
759
proc.returncode, args, sub_output)
762
'Unable to connect to environment' in sub_error or
763
'MissingOrIncorrectVersionHeader' in sub_error or
764
'307: Temporary Redirect' in sub_error):
765
raise CannotConnectEnv(e)
769
def pause(self, seconds):
773
class Juju2A2Backend(Juju2Backend):
776
Uses -m to specify models, uses JUJU_HOME and JUJU_DATA to specify home
780
def shell_environ(self, used_feature_flags, juju_home):
781
"""Generate a suitable shell environment.
783
For 2.0-alpha2 set both JUJU_HOME and JUJU_DATA.
785
env = super(Juju2A2Backend, self).shell_environ(used_feature_flags,
787
env['JUJU_HOME'] = juju_home
791
class Juju1XBackend(Juju2A2Backend):
792
"""Backend for Juju 1.x - 2A1.
794
Uses -e to specify models ("environments", uses JUJU_HOME to specify home
798
def shell_environ(self, used_feature_flags, juju_home):
799
"""Generate a suitable shell environment.
801
For 2.0-alpha1 and earlier set only JUJU_HOME and not JUJU_DATA.
803
env = super(Juju1XBackend, self).shell_environ(used_feature_flags,
805
env['JUJU_HOME'] = juju_home
809
def full_args(self, command, args, model, timeout):
813
# In 1.x terminology, "model" is "environment".
814
e_arg = ('-e', model)
818
prefix = get_timeout_prefix(timeout, self._timeout_path)
819
logging = '--debug' if self.debug else '--show-log'
821
# If args is a string, make it a tuple. This makes writing commands
822
# with one argument a bit nicer.
823
if isinstance(args, basestring):
825
# we split the command here so that the caller can control where the -e
826
# <env> flag goes. Everything in the command string is put before the
828
command = command.split()
829
return (prefix + (self.juju_name, logging,) + tuple(command) + e_arg +
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
860
elif re.match('^2\.0-beta(9|1[0-4])([^\d]|$)', version):
861
client_class = EnvJujuClient2B9
863
client_class = EnvJujuClient
867
def client_from_config(config, juju_path, debug=False, soft_deadline=None):
868
"""Create a client from an environment's configuration.
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
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()
883
full_path = os.path.abspath(juju_path)
884
return client_class(env, version, full_path, debug=debug,
885
soft_deadline=soft_deadline)
888
154
class EnvJujuClient:
889
"""Wraps calls to a juju instance, associated with a single model.
891
Note: A model is often called an enviroment (Juju 1 legacy).
893
This class represents the latest Juju version. Subclasses are used to
894
support older versions (see get_client_class).
897
# The environments.yaml options that are replaced by bootstrap options.
899
# As described in bug #1538735, default-series and --bootstrap-series must
900
# match. 'default-series' should be here, but is omitted so that
901
# default-series is always forced to match --bootstrap-series.
902
bootstrap_replaces = frozenset(['agent-version'])
904
# What feature flags have existed that CI used.
905
known_feature_flags = frozenset([
906
'actions', 'jes', 'address-allocation', 'cloudsigma', 'migration'])
908
# What feature flags are used by this version of the juju client.
909
used_feature_flags = frozenset(['address-allocation', 'migration'])
911
destroy_model_command = 'destroy-model'
913
supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE,
916
default_backend = Juju2Backend
918
config_class = JujuData
920
status_class = Status
922
agent_metadata_url = 'agent-metadata-url'
924
model_permissions = frozenset(['read', 'write', 'admin'])
926
controller_permissions = frozenset(['login', 'addmodel', 'superuser'])
929
def preferred_container(cls):
930
for container_type in [LXD_MACHINE, LXC_MACHINE]:
931
if container_type in cls.supported_container_types:
932
return container_type
934
_show_status = 'show-status'
937
157
def get_version(cls, juju_path=None):
938
"""Get the version data from a juju binary.
940
:param juju_path: Path to binary. If not given or None, 'juju' is used.
942
158
if juju_path is None:
943
159
juju_path = 'juju'
944
160
return subprocess.check_output((juju_path, '--version')).strip()
946
def check_timeouts(self):
947
return self._backend._check_timeouts()
949
def ignore_soft_deadline(self):
950
return self._backend.ignore_soft_deadline()
952
def enable_feature(self, flag):
953
"""Enable juju feature by setting the given flag.
955
New versions of juju with the feature enabled by default will silently
956
allow this call, but will not export the environment variable.
958
if flag not in self.known_feature_flags:
959
raise ValueError('Unknown feature flag: %r' % (flag,))
960
self.feature_flags.add(flag)
962
def get_jes_command(self):
963
"""For Juju 2.0, this is always kill-controller."""
964
return KILL_CONTROLLER
966
162
def is_jes_enabled(self):
967
163
"""Does the state-server support multiple environments."""
993
207
return WIN_JUJU_CMD
994
208
return subprocess.check_output(('which', 'juju')).rstrip('\n')
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)
211
def by_version(cls, env, juju_path=None, debug=False):
212
version = cls.get_version(juju_path)
1000
213
if juju_path is None:
1001
full_path = self.get_full_path()
214
full_path = cls.get_full_path()
1003
216
full_path = os.path.abspath(juju_path)
1004
return self.clone(version=version, full_path=full_path, cls=cls)
1006
def clone(self, env=None, version=None, full_path=None, debug=None,
1008
"""Create a clone of this EnvJujuClient.
1010
By default, the class, environment, version, full_path, and debug
1011
settings will match the original, but each can be overridden.
1016
cls = self.__class__
1017
feature_flags = self.feature_flags.intersection(cls.used_feature_flags)
1018
backend = self._backend.clone(full_path, version, debug, feature_flags)
1019
other = cls.from_backend(backend, env)
1023
def from_backend(cls, backend, env):
1024
return cls(env=env, version=backend.version,
1025
full_path=backend.full_path,
1026
debug=backend.debug, _backend=backend)
1028
def get_cache_path(self):
1029
return get_cache_path(self.env.juju_home, models=True)
1031
def _cmd_model(self, include_e, controller):
1033
return '{controller}:{model}'.format(
1034
controller=self.env.controller.name,
1035
model=self.get_controller_model_name())
1036
elif self.env is None or not include_e:
217
if version.startswith('1.16'):
218
raise Exception('Unsupported juju: %s' % version)
219
elif re.match('^1\.22[.-]', version):
220
client_class = EnvJujuClient22
221
elif re.match('^1\.24[.-]', version):
222
client_class = EnvJujuClient24
223
elif re.match('^1\.25[.-]', version):
224
client_class = EnvJujuClient25
225
elif re.match('^1\.26[.-]', version):
226
client_class = EnvJujuClient26
1039
return '{controller}:{model}'.format(
1040
controller=self.env.controller.name,
1041
model=self.model_name)
228
client_class = EnvJujuClient
229
return client_class(env, version, full_path, get_juju_home(),
1043
def _full_args(self, command, sudo, args,
1044
timeout=None, include_e=True, controller=False):
1045
model = self._cmd_model(include_e, controller)
232
def _full_args(self, command, sudo, args, timeout=None, include_e=True):
1046
233
# sudo is not needed for devel releases.
1047
return self._backend.full_args(command, args, model, timeout)
1051
if not isinstance(env, JujuData) and isinstance(env,
1053
# FIXME: JujuData should be used from the start.
1054
env = JujuData.from_env(env)
1057
def __init__(self, env, version, full_path, juju_home=None, debug=False,
1058
soft_deadline=None, _backend=None):
1059
"""Create a new juju client.
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.
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
1073
:param _backend: The backend to use for interacting with the client.
1074
If None (the default), self.default_backend will be used.
1076
self.env = self._get_env(env)
1077
if _backend is None:
1078
_backend = self.default_backend(full_path, version, set(), debug,
1080
self._backend = _backend
1081
if version != _backend.version:
1082
raise ValueError('Version mismatch: {} {}'.format(
1083
version, _backend.version))
1084
if full_path != _backend.full_path:
1085
raise ValueError('Path mismatch: {} {}'.format(
1086
full_path, _backend.full_path))
1087
if debug is not _backend.debug:
1088
raise ValueError('debug mismatch: {} {}'.format(
1089
debug, _backend.debug))
234
if self.env is None or not include_e:
237
e_arg = ('-e', self.env.environment)
241
prefix = get_timeout_prefix(timeout, self._timeout_path)
242
logging = '--debug' if self.debug else '--show-log'
244
# If args is a string, make it a tuple. This makes writing commands
245
# with one argument a bit nicer.
246
if isinstance(args, basestring):
248
# we split the command here so that the caller can control where the -e
249
# <env> flag goes. Everything in the command string is put before the
251
command = command.split()
252
return prefix + ('juju', logging,) + tuple(command) + e_arg + args
254
def __init__(self, env, version, full_path, juju_home=None, debug=False):
256
self.version = version
257
self.full_path = full_path
1090
259
if env is not None:
1091
260
if juju_home is None:
1092
261
if env.juju_home is None:
1093
262
env.juju_home = get_juju_home()
1095
264
env.juju_home = juju_home
1099
return self._backend.version
1102
def full_path(self):
1103
return self._backend.full_path
1106
def feature_flags(self):
1107
return self._backend.feature_flags
1109
@feature_flags.setter
1110
def feature_flags(self, feature_flags):
1111
self._backend.feature_flags = feature_flags
1115
return self._backend.debug
1118
def model_name(self):
1119
return self.env.environment
265
self.juju_timings = {}
266
self._timeout_path = get_timeout_path()
1121
268
def _shell_environ(self):
1122
269
"""Generate a suitable shell environment.
1124
271
Juju's directory must be in the PATH to support plugins.
1126
return self._backend.shell_environ(self.used_feature_flags,
273
env = dict(os.environ)
274
if self.full_path is not None:
275
env['PATH'] = '{}{}{}'.format(os.path.dirname(self.full_path),
276
os.pathsep, env['PATH'])
277
env['JUJU_HOME'] = self.env.juju_home
1129
280
def add_ssh_machines(self, machines):
1130
for count, machine in enumerate(machines):
1132
self.juju('add-machine', ('ssh:' + machine,))
1133
except subprocess.CalledProcessError:
1136
logging.warning('add-machine failed. Will retry.')
1138
self.juju('add-machine', ('ssh:' + machine,))
1141
def get_cloud_region(cloud, region):
1144
return '{}/{}'.format(cloud, region)
1146
def get_bootstrap_args(
1147
self, upload_tools, config_filename, bootstrap_series=None,
1148
credential=None, auto_upgrade=False, metadata_source=None,
1149
to=None, agent_version=None):
1150
"""Return the bootstrap arguments for the substrate."""
281
for machine in machines:
282
self.juju('add-machine', ('ssh:' + machine,))
284
def get_bootstrap_args(self, upload_tools):
285
"""Bootstrap, using sudo if necessary."""
287
constraints = 'mem=2G'
289
constraints = 'mem=2G arch=amd64'
290
elif self.env.joyent:
1152
291
# Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1153
292
constraints = 'mem=2G cpu-cores=1'
1155
294
constraints = 'mem=2G'
1156
cloud_region = self.get_cloud_region(self.env.get_cloud(),
1157
self.env.get_region())
1158
args = ['--constraints', constraints, self.env.environment,
1159
cloud_region, '--config', config_filename,
1160
'--default-model', self.env.environment]
295
args = ('--constraints', constraints)
1161
296
if upload_tools:
1162
if agent_version is not None:
1164
'agent-version may not be given with upload-tools.')
1165
args.insert(0, '--upload-tools')
1167
if agent_version is None:
1168
agent_version = self.get_matching_agent_version()
1169
args.extend(['--agent-version', agent_version])
1170
if bootstrap_series is not None:
1171
args.extend(['--bootstrap-series', bootstrap_series])
1172
if credential is not None:
1173
args.extend(['--credential', credential])
1174
if metadata_source is not None:
1175
args.extend(['--metadata-source', metadata_source])
1177
args.append('--auto-upgrade')
1179
args.extend(['--to', to])
1182
def add_model(self, env):
1183
"""Add a model to this model's controller and return its client.
1185
:param env: Class representing the new model/environment."""
1186
model_client = self.clone(env)
1187
with model_client._bootstrap_config() as config_file:
1188
self._add_model(env.environment, config_file)
1191
def make_model_config(self):
1192
config_dict = make_safe_config(self)
1193
agent_metadata_url = config_dict.pop('tools-metadata-url', None)
1194
if agent_metadata_url is not None:
1195
config_dict.setdefault('agent-metadata-url', agent_metadata_url)
1196
# Strip unneeded variables.
1197
return dict((k, v) for k, v in config_dict.items() if k not in {
1201
'application-password',
1210
'management-certificate',
1211
'management-subscription-id',
1222
'storage-account-name',
1231
def _bootstrap_config(self):
1232
with temp_yaml_file(self.make_model_config()) as config_filename:
1233
yield config_filename
1235
def _check_bootstrap(self):
1236
if self.env.environment != self.env.controller.name:
1237
raise AssertionError(
1238
'Controller and environment names should not vary (yet)')
1240
def update_user_name(self):
1241
self.env.user_name = 'admin@local'
1243
def bootstrap(self, upload_tools=False, bootstrap_series=None,
1244
credential=None, auto_upgrade=False, metadata_source=None,
1245
to=None, agent_version=None):
1246
"""Bootstrap a controller."""
1247
self._check_bootstrap()
1248
with self._bootstrap_config() as config_filename:
1249
args = self.get_bootstrap_args(
1250
upload_tools, config_filename, bootstrap_series, credential,
1251
auto_upgrade, metadata_source, to, agent_version)
1252
self.update_user_name()
1253
self.juju('bootstrap', args, include_e=False)
1256
def bootstrap_async(self, upload_tools=False, bootstrap_series=None,
1257
auto_upgrade=False, metadata_source=None, to=None):
1258
self._check_bootstrap()
1259
with self._bootstrap_config() as config_filename:
1260
args = self.get_bootstrap_args(
1261
upload_tools, config_filename, bootstrap_series, None,
1262
auto_upgrade, metadata_source, to)
1263
self.update_user_name()
1264
with self.juju_async('bootstrap', args, include_e=False):
1266
log.info('Waiting for bootstrap of {}.'.format(
1267
self.env.environment))
1269
def _add_model(self, model_name, config_file):
1270
self.controller_juju('add-model', (
1271
model_name, '--config', config_file))
1273
def destroy_model(self):
297
args = ('--upload-tools',) + args
300
def bootstrap(self, upload_tools=False):
301
args = self.get_bootstrap_args(upload_tools)
302
self.juju('bootstrap', args, self.env.needs_sudo())
305
def bootstrap_async(self, upload_tools=False):
306
args = self.get_bootstrap_args(upload_tools)
307
with self.juju_async('bootstrap', args):
309
log.info('Waiting for bootstrap of {}.'.format(
310
self.env.environment))
312
def create_environment(self, controller_client, config_file):
313
seen_cmd = self.get_jes_command()
314
if seen_cmd == OPTIONAL_JES_COMMAND:
315
controller_option = ('-s', controller_client.env.environment)
317
controller_option = ('-c', controller_client.env.environment)
318
self.juju(_jes_cmds[seen_cmd]['create'], controller_option + (
319
self.env.environment, '--config', config_file), include_e=False)
321
def destroy_environment(self, force=True, delete_jenv=False):
323
force_arg = ('--force',)
1274
326
exit_status = self.juju(
1275
'destroy-model', (self.env.environment, '-y',),
1276
include_e=False, timeout=get_teardown_timeout(self))
327
'destroy-environment',
328
(self.env.environment,) + force_arg + ('-y',),
329
self.env.needs_sudo(), check=False, include_e=False,
330
timeout=timedelta(minutes=10).total_seconds())
332
jenv_path = get_jenv_path(self.env.juju_home, self.env.environment)
333
ensure_deleted(jenv_path)
1277
334
return exit_status
1279
336
def kill_controller(self):
1280
337
"""Kill a controller and its environments."""
1281
338
seen_cmd = self.get_jes_command()
1283
_jes_cmds[seen_cmd]['kill'], (self.env.controller.name, '-y'),
1284
include_e=False, check=False, timeout=get_teardown_timeout(self))
340
_jes_cmds[seen_cmd]['kill'], (self.env.environment, '-y'),
341
include_e=False, check=False, timeout=600)
1286
343
def get_juju_output(self, command, *args, **kwargs):
1287
344
"""Call a juju command and return the output.
1290
347
that <command> may be a space delimited list of arguments. The -e
1291
348
<environment> flag will be placed after <command> and before args.
1293
model = self._cmd_model(kwargs.get('include_e', True),
1294
kwargs.get('controller', False))
1296
(k, kwargs[k]) for k in kwargs if k in ['timeout', 'merge_stderr'])
1297
return self._backend.get_juju_output(
1298
command, args, self.used_feature_flags, self.env.juju_home,
1299
model, user_name=self.env.user_name, **pass_kwargs)
1301
def show_status(self):
1302
"""Print the status to output."""
1303
self.juju(self._show_status, ('--format', 'yaml'))
1305
def get_status(self, timeout=60, raw=False, controller=False, *args):
350
args = self._full_args(command, False, args,
351
timeout=kwargs.get('timeout'),
352
include_e=kwargs.get('include_e', True))
353
env = self._shell_environ()
355
# Mutate os.environ instead of supplying env parameter so
356
# Windows can search env['PATH']
357
with scoped_environ(env):
358
proc = subprocess.Popen(
359
args, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
360
stderr=subprocess.PIPE)
361
sub_output, sub_error = proc.communicate()
362
log.debug(sub_output)
363
if proc.returncode != 0:
365
e = subprocess.CalledProcessError(
366
proc.returncode, args[0], sub_error)
369
'Unable to connect to environment' in sub_error or
370
'MissingOrIncorrectVersionHeader' in sub_error or
371
'307: Temporary Redirect' in sub_error):
372
raise CannotConnectEnv(e)
376
def get_status(self, timeout=60, raw=False, *args):
1306
377
"""Get the current status as a dict."""
1307
378
# GZ 2015-12-16: Pass remaining timeout into get_juju_output call.
1308
379
for ignored in until_timeout(timeout):
1311
return self.get_juju_output(self._show_status, *args)
1312
return self.status_class.from_text(
1313
self.get_juju_output(
1314
self._show_status, '--format', 'yaml',
1315
controller=controller))
382
return self.get_juju_output('status', *args)
383
return Status.from_text(self.get_juju_output('status'))
1316
384
except subprocess.CalledProcessError:
1318
386
raise Exception(
1319
387
'Timed out waiting for juju status to succeed')
1322
def _dict_as_option_strings(options):
1323
return tuple('{}={}'.format(*item) for item in options.items())
1325
def set_config(self, service, options):
1326
option_strings = self._dict_as_option_strings(options)
1327
self.juju('config', (service,) + option_strings)
1329
def get_config(self, service):
1330
return yaml_loads(self.get_juju_output('config', service))
1332
389
def get_service_config(self, service, timeout=60):
1333
390
for ignored in until_timeout(timeout):
1335
return self.get_config(service)
392
return yaml_loads(self.get_juju_output('get', service))
1336
393
except subprocess.CalledProcessError:
1338
395
raise Exception(
1339
396
'Timed out waiting for juju get %s' % (service))
1341
def set_model_constraints(self, constraints):
1342
constraint_strings = self._dict_as_option_strings(constraints)
1343
return self.juju('set-model-constraints', constraint_strings)
1345
def get_model_config(self):
1346
"""Return the value of the environment's configured options."""
1347
return yaml.safe_load(
1348
self.get_juju_output('model-config', '--format', 'yaml'))
1350
398
def get_env_option(self, option):
1351
399
"""Return the value of the environment's configured option."""
1352
return self.get_juju_output('model-config', option)
400
return self.get_juju_output('get-env', option)
1354
402
def set_env_option(self, option, value):
1355
403
"""Set the value of the option in the environment."""
1356
404
option_value = "%s=%s" % (option, value)
1357
return self.juju('model-config', (option_value,))
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,))
1363
def get_agent_metadata_url(self):
1364
return self.get_env_option(self.agent_metadata_url)
1366
def set_testing_agent_metadata_url(self):
1367
url = self.get_agent_metadata_url()
405
return self.juju('set-env', (option_value,))
407
def set_testing_tools_metadata_url(self):
408
url = self.get_env_option('tools-metadata-url')
1368
409
if 'testing' not in url:
1369
410
testing_url = url.replace('/tools', '/testing/tools')
1370
self.set_env_option(self.agent_metadata_url, testing_url)
411
self.set_env_option('tools-metadata-url', testing_url)
1372
413
def juju(self, command, args, sudo=False, check=True, include_e=True,
1373
414
timeout=None, extra_env=None):
1374
415
"""Run a command under juju for the current environment."""
1375
model = self._cmd_model(include_e, controller=False)
1376
return self._backend.juju(
1377
command, args, self.used_feature_flags, self.env.juju_home,
1378
model, check, timeout, extra_env)
1380
def expect(self, command, args=(), sudo=False, include_e=True,
1381
timeout=None, extra_env=None):
1382
"""Return a process object that is running an interactive `command`.
1384
The interactive command ability is provided by using pexpect.
1386
:param command: String of the juju command to run.
1387
:param args: Tuple containing arguments for the juju `command`.
1388
:param sudo: Whether to call `command` using sudo.
1389
:param include_e: Boolean regarding supplying the juju environment to
1391
:param timeout: A float that, if provided, is the timeout in which the
1394
:return: A pexpect.spawn object that has been called with `command` and
1398
model = self._cmd_model(include_e, controller=False)
1399
return self._backend.expect(
1400
command, args, self.used_feature_flags, self.env.juju_home,
1401
model, timeout, extra_env)
1403
def controller_juju(self, command, args):
1404
args = ('-c', self.env.controller.name) + args
1405
return self.juju(command, args, include_e=False)
416
args = self._full_args(command, sudo, args, include_e=include_e,
418
log.info(' '.join(args))
419
env = self._shell_environ()
420
if extra_env is not None:
421
env.update(extra_env)
423
call_func = subprocess.check_call
425
call_func = subprocess.call
426
start_time = time.time()
427
# Mutate os.environ instead of supplying env parameter so Windows can
429
with scoped_environ(env):
430
rval = call_func(args)
431
self.juju_timings.setdefault(args, []).append(
432
(time.time() - start_time))
1407
435
def get_juju_timings(self):
1408
436
stringified_timings = {}
1409
for command, timings in self._backend.juju_timings.items():
437
for command, timings in self.juju_timings.items():
1410
438
stringified_timings[' '.join(command)] = timings
1411
439
return stringified_timings
1413
442
def juju_async(self, command, args, include_e=True, timeout=None):
1414
model = self._cmd_model(include_e, controller=False)
1415
return self._backend.juju_async(command, args, self.used_feature_flags,
1416
self.env.juju_home, model, timeout)
443
full_args = self._full_args(command, False, args, include_e=include_e,
445
log.info(' '.join(args))
446
env = self._shell_environ()
447
# Mutate os.environ instead of supplying env parameter so Windows can
449
with scoped_environ(env):
450
proc = subprocess.Popen(full_args)
452
retcode = proc.wait()
454
raise subprocess.CalledProcessError(retcode, full_args)
1418
def deploy(self, charm, repository=None, to=None, series=None,
1419
service=None, force=False, resource=None,
1420
storage=None, constraints=None):
456
def deploy(self, charm, repository=None, to=None, service=None):
458
if repository is not None:
459
args.extend(['--repository', repository])
461
args.extend(['--to', to])
1422
462
if service is not None:
1423
463
args.extend([service])
1425
args.extend(['--to', to])
1426
if series is not None:
1427
args.extend(['--series', series])
1429
args.extend(['--force'])
1430
if resource is not None:
1431
args.extend(['--resource', resource])
1432
if storage is not None:
1433
args.extend(['--storage', storage])
1434
if constraints is not None:
1435
args.extend(['--constraints', constraints])
1436
464
return self.juju('deploy', tuple(args))
1438
def attach(self, service, resource):
1439
args = (service, resource)
1440
return self.juju('attach', args)
1442
def list_resources(self, service_or_unit, details=True):
1443
args = ('--format', 'yaml', service_or_unit)
1445
args = args + ('--details',)
1446
return yaml_loads(self.get_juju_output('list-resources', *args))
1448
def wait_for_resource(self, resource_id, service_or_unit, timeout=60):
1449
log.info('Waiting for resource. Resource id:{}'.format(resource_id))
1450
with self.check_timeouts():
1451
with self.ignore_soft_deadline():
1452
for _ in until_timeout(timeout):
1453
resources_dict = self.list_resources(service_or_unit)
1454
resources = resources_dict['resources']
1455
for resource in resources:
1456
if resource['expected']['resourceid'] == resource_id:
1457
if (resource['expected']['fingerprint'] ==
1458
resource['unit']['fingerprint']):
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))
1466
def upgrade_charm(self, service, charm_path=None):
1468
if charm_path is not None:
1469
args = args + ('--path', charm_path)
1470
self.juju('upgrade-charm', args)
1472
def remove_service(self, service):
1473
self.juju('remove-application', (service,))
1476
def format_bundle(cls, bundle_template):
1477
return bundle_template.format(container=cls.preferred_container())
1479
def deploy_bundle(self, bundle_template, timeout=_DEFAULT_BUNDLE_TIMEOUT):
1480
"""Deploy bundle using native juju 2.0 deploy command."""
1481
bundle = self.format_bundle(bundle_template)
1482
self.juju('deploy', bundle, timeout=timeout)
1484
def deployer(self, bundle_template, name=None, deploy_delay=10,
1486
"""Deploy a bundle using deployer."""
1487
bundle = self.format_bundle(bundle_template)
466
def deployer(self, bundle, name=None, deploy_delay=10, timeout=3600):
467
"""deployer, using sudo if necessary."""
1490
470
'--deploy-delay', str(deploy_delay),
1610
576
self._wait_for_status(reporter, status_to_version, VersionsNotUpdated,
1611
577
timeout=timeout, start=start)
1613
def list_models(self):
1614
"""List the models registered with the current controller."""
1615
self.controller_juju('list-models', ())
1617
def get_models(self):
1618
"""return a models dict with a 'models': [] key-value pair.
1620
The server has 120 seconds to respond because this method is called
1621
often when tearing down a controller-less deployment.
1623
output = self.get_juju_output(
1624
'list-models', '-c', self.env.controller.name, '--format', 'yaml',
1625
include_e=False, timeout=120)
1626
models = yaml_loads(output)
1629
def _get_models(self):
1630
"""return a list of model dicts."""
1631
return self.get_models()['models']
1633
def iter_model_clients(self):
1634
"""Iterate through all the models that share this model's controller.
1636
Works only if JES is enabled.
1638
models = self._get_models()
1641
for model in models:
1642
yield self._acquire_model_client(model['name'])
1644
def get_controller_model_name(self):
1645
"""Return the name of the 'controller' model.
1647
Return the name of the environment when an 'controller' model does
1652
def _acquire_model_client(self, name):
1653
"""Get a client for a model with the supplied name.
1655
If the name matches self, self is used. Otherwise, a clone is used.
1657
if name == self.env.environment:
1660
env = self.env.clone(model_name=name)
1661
return self.clone(env=env)
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']
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']
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']
1684
def get_controller_client(self):
1685
"""Return a client for the controller model. May return self.
1687
This may be inaccurate for models created using add_model
1688
rather than bootstrap.
1690
return self._acquire_model_client(self.get_controller_model_name())
1692
def list_controllers(self):
1693
"""List the controllers."""
1694
self.juju('list-controllers', (), include_e=False)
1696
def get_controller_endpoint(self):
1697
"""Return the address of the controller leader."""
1698
controller = self.env.controller.name
1699
output = self.get_juju_output(
1700
'show-controller', controller, include_e=False)
1701
info = yaml_loads(output)
1702
endpoint = info[controller]['details']['api-endpoints'][0]
1703
address, port = split_address_port(endpoint)
1706
def get_controller_members(self):
1707
"""Return a list of Machines that are members of the controller.
1709
The first machine in the list is the leader. the remaining machines
1710
are followers in a HA relationship.
1713
status = self.get_status()
1714
for machine_id, machine in status.iter_machines():
1715
if self.get_controller_member_status(machine):
1716
members.append(Machine(machine_id, machine))
1717
if len(members) <= 1:
1719
# Search for the leader and make it the first in the list.
1720
# If the endpoint address is not the same as the leader's dns_name,
1721
# the members are return in the order they were discovered.
1722
endpoint = self.get_controller_endpoint()
1723
log.debug('Controller endpoint is at {}'.format(endpoint))
1724
members.sort(key=lambda m: m.info.get('dns-name') != endpoint)
1727
def get_controller_leader(self):
1728
"""Return the controller leader Machine."""
1729
controller_members = self.get_controller_members()
1730
return controller_members[0]
1733
def get_controller_member_status(info_dict):
1734
"""Return the controller-member-status of the machine if it exists."""
1735
return info_dict.get('controller-member-status')
1737
579
def wait_for_ha(self, timeout=1200):
1738
580
desired_state = 'has-vote'
1739
581
reporter = GroupReporter(sys.stdout, desired_state)
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()
1747
for machine, info in status.iter_machines():
1748
status = self.get_controller_member_status(info)
1751
states.setdefault(status, []).append(machine)
1752
if states.keys() == [desired_state]:
1753
if len(states.get(desired_state, [])) >= 3:
1755
reporter.update(states)
1757
raise Exception('Timed out waiting for voting to be'
583
for remaining in until_timeout(timeout):
584
status = self.get_status()
586
for machine, info in status.iter_machines():
587
status = info.get('state-server-member-status')
590
states.setdefault(status, []).append(machine)
591
if states.keys() == [desired_state]:
592
if len(states.get(desired_state, [])) >= 3:
593
# XXX sinzui 2014-12-04: bug 1399277 happens because
594
# juju claims HA is ready when the monogo replica sets
595
# are not. Juju is not fully usable. The replica set
596
# lag might be 5 minutes.
599
reporter.update(states)
601
raise Exception('Timed out waiting for voting to be enabled.')
1760
603
reporter.finish()
1761
# XXX sinzui 2014-12-04: bug 1399277 happens because
1762
# juju claims HA is ready when the monogo replica sets
1763
# are not. Juju is not fully usable. The replica set
1764
# lag might be 5 minutes.
1765
self._backend.pause(300)
1767
605
def wait_for_deploy_started(self, service_count=1, timeout=1200):
1768
606
"""Wait until service_count services are 'started'.
1863
699
if force_version:
1864
700
version = self.get_matching_agent_version(no_build=True)
1865
args += ('--agent-version', version)
1866
self._upgrade_juju(args)
1868
def _upgrade_juju(self, args):
701
args += ('--version', version)
703
args += ('--upload-tools',)
1869
704
self.juju('upgrade-juju', args)
1871
def upgrade_mongo(self):
1872
self.juju('upgrade-mongo', ())
1876
output = self.get_juju_output('create-backup')
1877
except subprocess.CalledProcessError as e:
1881
backup_file_pattern = re.compile('(juju-backup-[0-9-]+\.(t|tar.)gz)')
1882
match = backup_file_pattern.search(output)
1884
raise Exception("The backup file was not found in output: %s" %
1886
backup_file_name = match.group(1)
1887
backup_file_path = os.path.abspath(backup_file_name)
1888
log.info("State-Server backup at %s", backup_file_path)
1889
return backup_file_path
1891
def restore_backup(self, backup_file):
1894
('-b', '--constraints', 'mem=2G', '--file', backup_file))
1896
def restore_backup_async(self, backup_file):
1897
return self.juju_async('restore-backup', ('-b', '--constraints',
1898
'mem=2G', '--file', backup_file))
1900
def enable_ha(self):
1901
self.juju('enable-ha', ('-n', '3'))
1903
def action_fetch(self, id, action=None, timeout="1m"):
1904
"""Fetches the results of the action with the given id.
1906
Will wait for up to 1 minute for the action results.
1907
The action name here is just used for an more informational error in
1908
cases where it's available.
1909
Returns the yaml output of the fetched action.
1911
out = self.get_juju_output("show-action-output", id, "--wait", timeout)
1912
status = yaml_loads(out)["status"]
1913
if status != "completed":
1915
if action is not None:
1918
"timed out waiting for action%s to complete during fetch" %
1922
def action_do(self, unit, action, *args):
1923
"""Performs the given action on the given unit.
1925
Action params should be given as args in the form foo=bar.
1926
Returns the id of the queued action.
1928
args = (unit, action) + args
1930
output = self.get_juju_output("run-action", *args)
1931
action_id_pattern = re.compile(
1932
'Action queued with id: ([a-f0-9\-]{36})')
1933
match = action_id_pattern.search(output)
1935
raise Exception("Action id not found in output: %s" %
1937
return match.group(1)
1939
def action_do_fetch(self, unit, action, timeout="1m", *args):
1940
"""Performs given action on given unit and waits for the results.
1942
Action params should be given as args in the form foo=bar.
1943
Returns the yaml output of the action.
1945
id = self.action_do(unit, action, *args)
1946
return self.action_fetch(id, action, timeout)
1948
def run(self, commands, applications):
1949
responses = self.get_juju_output(
1950
'run', '--format', 'json', '--application', ','.join(applications),
1952
return json.loads(responses)
1954
def list_space(self):
1955
return yaml.safe_load(self.get_juju_output('list-space'))
1957
def add_space(self, space):
1958
self.juju('add-space', (space),)
1960
def add_subnet(self, subnet, space):
1961
self.juju('add-subnet', (subnet, space))
1963
def is_juju1x(self):
1964
return self.version.startswith('1.')
1966
def _get_register_command(self, output):
1967
"""Return register token from add-user output.
1969
Return the register token supplied within the output from the add-user
1973
for row in output.split('\n'):
1974
if 'juju register' in row:
1975
command_string = row.strip().lstrip()
1976
command_parts = command_string.split(' ')
1977
return command_parts[-1]
1978
raise AssertionError('Juju register command not found in output')
1980
def add_user(self, username):
1981
"""Adds provided user and return register command arguments.
1983
:return: Registration token provided by the add-user command.
1985
output = self.get_juju_output(
1986
'add-user', username, '-c', self.env.controller.name,
1988
return self._get_register_command(output)
1990
def add_user_perms(self, username, models=None, permissions='login'):
1991
"""Adds provided user and return register command arguments.
1993
:return: Registration token provided by the add-user command.
1995
output = self.add_user(username)
1996
self.grant(username, permissions, models)
1999
def revoke(self, username, models=None, permissions='read'):
2001
models = self.env.environment
2003
args = (username, permissions, models)
2005
self.controller_juju('revoke', args)
2007
def add_storage(self, unit, storage_type, amount="1"):
2008
"""Add storage instances to service.
2010
Only type 'disk' is able to add instances.
2012
self.juju('add-storage', (unit, storage_type + "=" + amount))
2014
def list_storage(self):
2015
"""Return the storage list."""
2016
return self.get_juju_output('list-storage', '--format', 'json')
2018
def list_storage_pool(self):
2019
"""Return the list of storage pool."""
2020
return self.get_juju_output('list-storage-pools', '--format', 'json')
2022
def create_storage_pool(self, name, provider, size):
2023
"""Create storage pool."""
2024
self.juju('create-storage-pool',
2026
'size={}'.format(size)))
2028
def disable_user(self, user_name):
2029
"""Disable an user"""
2030
self.controller_juju('disable-user', (user_name,))
2032
def enable_user(self, user_name):
2033
"""Enable an user"""
2034
self.controller_juju('enable-user', (user_name,))
2037
"""Logout an user"""
2038
self.controller_juju('logout', ())
2039
self.env.user_name = ''
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)
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,
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)
2065
'Registering user failed: pexpect session still alive')
2066
except pexpect.TIMEOUT:
2068
'Registering user failed: pexpect session timed out')
2069
user_client.env.user_name = username
2072
def remove_user(self, username):
2073
self.juju('remove-user', (username, '-y'), include_e=False)
2075
def create_cloned_environment(
2076
self, cloned_juju_home, controller_name, user_name=None):
2077
"""Create a cloned environment.
2079
If `user_name` is passed ensures that the cloned environment is updated
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)
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:
2098
(user_name, permission, '-c', self.env.controller.name),
2100
elif permission in self.model_permissions:
2102
model = self.model_name
2105
(user_name, permission, model, '-c', self.env.controller.name),
2108
raise ValueError('Unknown permission {}'.format(permission))
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)
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)
2120
def ssh_keys(self, full=False):
2121
"""Give the ssh keys registered for the current model."""
2124
args.append('--full')
2125
return self.get_juju_output('ssh-keys', *args)
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)
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)
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)
2139
def list_disabled_commands(self):
2140
"""List all the commands disabled on the model."""
2141
raw = self.get_juju_output('list-disabled-commands',
2143
return yaml.safe_load(raw)
2145
def disable_command(self, args):
2146
"""Disable a command set."""
2147
return self.juju('disable-command', args)
2149
def enable_command(self, args):
2150
"""Enable a command set."""
2151
return self.juju('enable-command', args)
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', ())
2158
return self.juju('sync-tools', ('--local-dir', local_dir),
2162
class EnvJujuClient2B9(EnvJujuClient):
2164
def update_user_name(self):
2167
def create_cloned_environment(
2168
self, cloned_juju_home, controller_name, user_name=None):
2169
"""Create a cloned environment.
2171
`user_name` is unused in this version of beta.
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)
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']
2185
def add_user_perms(self, username, models=None, permissions='read'):
2186
"""Adds provided user and return register command arguments.
2188
:return: Registration token provided by the add-user command.
2192
models = self.env.environment
2194
args = (username, '--models', models, '--acl', permissions,
2195
'-c', self.env.controller.name)
2197
output = self.get_juju_output('add-user', *args, include_e=False)
2198
return self._get_register_command(output)
2200
def grant(self, user_name, permission, model=None):
2201
"""Grant the user with a model."""
2203
model = self.model_name
2204
self.juju('grant', (user_name, model, '--acl', permission),
2207
def revoke(self, username, models=None, permissions='read'):
2209
models = self.env.environment
2211
args = (username, models, '--acl', permissions)
2213
self.controller_juju('revoke', args)
2215
def set_config(self, service, options):
2216
option_strings = self._dict_as_option_strings(options)
2217
self.juju('set-config', (service,) + option_strings)
2219
def get_config(self, service):
2220
return yaml_loads(self.get_juju_output('get-config', service))
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'))
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)
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,))
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,))
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)
2245
def disable_command(self, args):
2246
"""Disable a command set."""
2247
return self.juju('block', args)
2249
def enable_command(self, args):
2250
"""Enable a command set."""
2251
return self.juju('unblock', args)
2254
class EnvJujuClient2B8(EnvJujuClient2B9):
2256
status_class = ServiceStatus
2258
def remove_service(self, service):
2259
self.juju('remove-service', (service,))
2261
def run(self, commands, applications):
2262
responses = self.get_juju_output(
2263
'run', '--format', 'json', '--service', ','.join(applications),
2265
return json.loads(responses)
2267
def deployer(self, bundle_template, name=None, deploy_delay=10,
2269
"""Deploy a bundle using deployer."""
2270
bundle = self.format_bundle(bundle_template)
2273
'--deploy-delay', str(deploy_delay),
2274
'--timeout', str(timeout),
2279
e_arg = ('-e', 'local.{}:{}'.format(
2280
self.env.controller.name, self.env.environment))
2282
self.juju('deployer', args, self.env.needs_sudo(), include_e=False)
2285
class EnvJujuClient2B7(EnvJujuClient2B8):
2287
def get_controller_model_name(self):
2288
"""Return the name of the 'controller' model.
2290
Return the name of the environment when an 'controller' model does
2296
class EnvJujuClient2B3(EnvJujuClient2B7):
2298
def _add_model(self, model_name, config_file):
2299
self.controller_juju('create-model', (
2300
model_name, '--config', config_file))
2303
class EnvJujuClient2B2(EnvJujuClient2B3):
2305
def get_bootstrap_args(
2306
self, upload_tools, config_filename, bootstrap_series=None,
2307
credential=None, auto_upgrade=False, metadata_source=None,
2308
to=None, agent_version=None):
2309
"""Return the bootstrap arguments for the substrate."""
2310
err_fmt = 'EnvJujuClient2B2 does not support bootstrap argument {}'
2312
raise ValueError(err_fmt.format('auto_upgrade'))
2313
if metadata_source is not None:
2314
raise ValueError(err_fmt.format('metadata_source'))
2316
raise ValueError(err_fmt.format('to'))
2317
if agent_version is not None:
2318
raise ValueError(err_fmt.format('agent_version'))
2320
# Only accept kvm packages by requiring >1 cpu core, see lp:1446264
2321
constraints = 'mem=2G cpu-cores=1'
2323
constraints = 'mem=2G'
2324
cloud_region = self.get_cloud_region(self.env.get_cloud(),
2325
self.env.get_region())
2326
args = ['--constraints', constraints, self.env.environment,
2327
cloud_region, '--config', config_filename]
2329
args.insert(0, '--upload-tools')
2331
args.extend(['--agent-version', self.get_matching_agent_version()])
2333
if bootstrap_series is not None:
2334
args.extend(['--bootstrap-series', bootstrap_series])
2336
if credential is not None:
2337
args.extend(['--credential', credential])
2341
def get_controller_client(self):
2342
"""Return a client for the controller model. May return self."""
2345
def get_controller_model_name(self):
2346
"""Return the name of the 'controller' model.
2348
Return the name of the environment when an 'controller' model does
2351
models = self.get_models()
2352
# The dict can be empty because 1.x does not support the models.
2353
# This is an ambiguous case for the jes feature flag which supports
2354
# multiple models, but none is named 'admin' by default. Since the
2355
# jes case also uses '-e' for models, the env is the controller model.
2356
for model in models.get('models', []):
2357
if 'admin' in model['name']:
2359
return self.env.environment
2362
class EnvJujuClient2A2(EnvJujuClient2B2):
2363
"""Drives Juju 2.0-alpha2 clients."""
2365
default_backend = Juju2A2Backend
2367
config_class = SimpleEnvironment
2370
def _get_env(cls, env):
2371
if isinstance(env, JujuData):
2372
raise IncompatibleConfigClass(
2373
'JujuData cannot be used with {}'.format(cls.__name__))
2376
def bootstrap(self, upload_tools=False, bootstrap_series=None):
2377
"""Bootstrap a controller."""
2378
self._check_bootstrap()
2379
args = self.get_bootstrap_args(upload_tools, bootstrap_series)
2380
self.juju('bootstrap', args, self.env.needs_sudo())
2383
def bootstrap_async(self, upload_tools=False):
2384
self._check_bootstrap()
2385
args = self.get_bootstrap_args(upload_tools)
2386
with self.juju_async('bootstrap', args):
2388
log.info('Waiting for bootstrap of {}.'.format(
2389
self.env.environment))
2391
def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2393
"""Return the bootstrap arguments for the substrate."""
2394
if credential is not None:
2396
'--credential is not supported by this juju version.')
2397
constraints = self._get_substrate_constraints()
2398
args = ('--constraints', constraints,
2399
'--agent-version', self.get_matching_agent_version())
2401
args = ('--upload-tools',) + args
2402
if bootstrap_series is not None:
2403
args = args + ('--bootstrap-series', bootstrap_series)
2406
def deploy(self, charm, repository=None, to=None, series=None,
2407
service=None, force=False, storage=None, constraints=None):
2409
if repository is not None:
2410
args.extend(['--repository', repository])
2412
args.extend(['--to', to])
2413
if service is not None:
2414
args.extend([service])
2415
if storage is not None:
2416
args.extend(['--storage', storage])
2417
if constraints is not None:
2418
args.extend(['--constraints', constraints])
2419
return self.juju('deploy', tuple(args))
2422
class EnvJujuClient2A1(EnvJujuClient2A2):
2423
"""Drives Juju 2.0-alpha1 clients."""
2425
_show_status = 'status'
2427
default_backend = Juju1XBackend
2429
def get_cache_path(self):
2430
return get_cache_path(self.env.juju_home, models=False)
2432
def remove_service(self, service):
2433
self.juju('destroy-service', (service,))
2435
706
def backup(self):
2436
707
environ = self._shell_environ()
2437
708
# juju-backup does not support the -e flag.
2540
767
return match.group(1)
2542
def list_space(self):
2543
return yaml.safe_load(self.get_juju_output('space list'))
2545
def add_space(self, space):
2546
self.juju('space create', (space),)
2548
def add_subnet(self, subnet, space):
2549
self.juju('subnet add', (subnet, space))
2551
def set_model_constraints(self, constraints):
2552
constraint_strings = self._dict_as_option_strings(constraints)
2553
return self.juju('set-constraints', constraint_strings)
2555
def set_config(self, service, options):
2556
option_strings = ['{}={}'.format(*item) for item in options.items()]
2557
self.juju('set', (service,) + tuple(option_strings))
2559
def get_config(self, service):
2560
return yaml_loads(self.get_juju_output('get', service))
2562
def get_model_config(self):
2563
"""Return the value of the environment's configured option."""
2564
return yaml.safe_load(self.get_juju_output('get-env'))
2566
def get_env_option(self, option):
2567
"""Return the value of the environment's configured option."""
2568
return self.get_juju_output('get-env', option)
2570
def set_env_option(self, option, value):
2571
"""Set the value of the option in the environment."""
2572
option_value = "%s=%s" % (option, value)
2573
return self.juju('set-env', (option_value,))
2576
class EnvJujuClient1X(EnvJujuClient2A1):
2577
"""Base for all 1.x client drivers."""
2579
# The environments.yaml options that are replaced by bootstrap options.
2580
# For Juju 1.x, no bootstrap options are used.
2581
bootstrap_replaces = frozenset()
2583
destroy_model_command = 'destroy-environment'
2585
supported_container_types = frozenset([KVM_MACHINE, LXC_MACHINE])
2587
agent_metadata_url = 'tools-metadata-url'
2589
def _cmd_model(self, include_e, controller):
2591
return self.get_controller_model_name()
2592
elif self.env is None or not include_e:
2595
return unqualified_model_name(self.model_name)
2597
def get_bootstrap_args(self, upload_tools, bootstrap_series=None,
2599
"""Return the bootstrap arguments for the substrate."""
2600
if credential is not None:
2602
'--credential is not supported by this juju version.')
2603
constraints = self._get_substrate_constraints()
2604
args = ('--constraints', constraints)
2606
args = ('--upload-tools',) + args
2607
if bootstrap_series is not None:
2608
env_val = self.env.config.get('default-series')
2609
if bootstrap_series != env_val:
2610
raise BootstrapMismatch(
2611
'bootstrap-series', bootstrap_series, 'default-series',
2615
def get_jes_command(self):
2616
"""Return the JES command to destroy a controller.
2618
Juju 2.x has 'kill-controller'.
2619
Some intermediate versions had 'controller kill'.
2620
Juju 1.25 has 'system kill' when the jes feature flag is set.
2622
:raises: JESNotSupported when the version of Juju does not expose
2624
:return: The JES command.
2626
commands = self.get_juju_output('help', 'commands', include_e=False)
2627
for line in commands.splitlines():
2628
for cmd in _jes_cmds.keys():
2629
if line.startswith(cmd):
2631
raise JESNotSupported()
2633
def upgrade_juju(self, force_version=True):
2636
version = self.get_matching_agent_version(no_build=True)
2637
args += ('--version', version)
2639
args += ('--upload-tools',)
2640
self._upgrade_juju(args)
2642
def make_model_config(self):
2643
config_dict = make_safe_config(self)
2644
# Strip unneeded variables.
2647
def _add_model(self, model_name, config_file):
2648
seen_cmd = self.get_jes_command()
2649
if seen_cmd == SYSTEM:
2650
controller_option = ('-s', self.env.environment)
2652
controller_option = ('-c', self.env.environment)
2653
self.juju(_jes_cmds[seen_cmd]['create'], controller_option + (
2654
model_name, '--config', config_file), include_e=False)
2656
def destroy_model(self):
2657
"""With JES enabled, destroy-environment destroys the model."""
2658
self.destroy_environment(force=False)
2660
def destroy_environment(self, force=True, delete_jenv=False):
2662
force_arg = ('--force',)
2665
exit_status = self.juju(
2666
'destroy-environment',
2667
(self.env.environment,) + force_arg + ('-y',),
2668
self.env.needs_sudo(), check=False, include_e=False,
2669
timeout=get_teardown_timeout(self))
2671
jenv_path = get_jenv_path(self.env.juju_home, self.env.environment)
2672
ensure_deleted(jenv_path)
2675
def _get_models(self):
2676
"""return a list of model dicts."""
2678
return yaml.safe_load(self.get_juju_output(
2679
'environments', '-s', self.env.environment, '--format', 'yaml',
2681
except subprocess.CalledProcessError:
2682
# This *private* method attempts to use a 1.25 JES feature.
2683
# The JES design is dead. The private method is not used to
2684
# directly test juju cli; the failure is not a contract violation.
2685
log.info('Call to JES juju environments failed, falling back.')
2688
def deploy_bundle(self, bundle, timeout=_DEFAULT_BUNDLE_TIMEOUT):
2689
"""Deploy bundle using deployer for Juju 1.X version."""
2690
self.deployer(bundle, timeout=timeout)
2692
def deployer(self, bundle_template, name=None, deploy_delay=10,
2694
"""Deploy a bundle using deployer."""
2695
bundle = self.format_bundle(bundle_template)
2698
'--deploy-delay', str(deploy_delay),
2699
'--timeout', str(timeout),
2704
self.juju('deployer', args, self.env.needs_sudo())
2706
def upgrade_charm(self, service, charm_path=None):
2708
if charm_path is not None:
2709
repository = os.path.dirname(os.path.dirname(charm_path))
2710
args = args + ('--repository', repository)
2711
self.juju('upgrade-charm', args)
2713
def get_controller_endpoint(self):
2714
"""Return the address of the state-server leader."""
2715
endpoint = self.get_juju_output('api-endpoints')
2716
address, port = split_address_port(endpoint)
2719
def upgrade_mongo(self):
2720
raise UpgradeMongoNotSupported()
2722
def add_storage(self, unit, storage_type, amount="1"):
2723
"""Add storage instances to service.
2725
Only type 'disk' is able to add instances.
2727
self.juju('storage add', (unit, storage_type + "=" + amount))
2729
def list_storage(self):
2730
"""Return the storage list."""
2731
return self.get_juju_output('storage list', '--format', 'json')
2733
def list_storage_pool(self):
2734
"""Return the list of storage pool."""
2735
return self.get_juju_output('storage pool list', '--format', 'json')
2737
def create_storage_pool(self, name, provider, size):
2738
"""Create storage pool."""
2739
self.juju('storage pool create',
2741
'size={}'.format(size)))
2743
def ssh_keys(self, full=False):
2744
"""Give the ssh keys registered for the current model."""
2747
args.append('--full')
2748
return self.get_juju_output('authorized-keys list', *args)
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,
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,
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,
2766
class EnvJujuClient22(EnvJujuClient1X):
2768
used_feature_flags = frozenset(['actions'])
2770
def __init__(self, *args, **kwargs):
2771
super(EnvJujuClient22, self).__init__(*args, **kwargs)
2772
self.feature_flags.add('actions')
2775
class EnvJujuClient26(EnvJujuClient1X):
769
def action_do_fetch(self, unit, action, timeout="1m", *args):
770
"""Performs given action on given unit and waits for the results.
772
Action params should be given as args in the form foo=bar.
773
Returns the yaml output of the action.
775
id = self.action_do(unit, action, *args)
776
return self.action_fetch(id, action, timeout)
779
class EnvJujuClient22(EnvJujuClient):
781
def _shell_environ(self):
782
"""Generate a suitable shell environment.
784
Juju's directory must be in the PATH to support plugins.
786
env = super(EnvJujuClient22, self)._shell_environ()
787
env[JUJU_DEV_FEATURE_FLAGS] = 'actions'
791
class EnvJujuClient26(EnvJujuClient):
2776
792
"""Drives Juju 2.6-series clients."""
2778
used_feature_flags = frozenset(['address-allocation', 'cloudsigma', 'jes'])
2780
794
def __init__(self, *args, **kwargs):
2781
795
super(EnvJujuClient26, self).__init__(*args, **kwargs)
2782
if self.env is None or self.env.config is None:
2784
if self.env.config.get('type') == 'cloudsigma':
2785
self.feature_flags.add('cloudsigma')
796
self._use_jes = False
797
self._use_container_address_allocation = False
2787
799
def enable_jes(self):
2788
800
"""Enable JES if JES is optional.
3047
"""Represents the controller for a model or models."""
3049
def __init__(self, name):
1071
def __init__(self, status, status_text):
1072
self.status = status
1073
self.status_text = status_text
1076
def from_text(cls, text):
1077
status_yaml = yaml_loads(text)
1078
return cls(status_yaml, text)
1080
def iter_machines(self, containers=False, machines=True):
1081
for machine_name, machine in sorted(self.status['machines'].items()):
1083
yield machine_name, machine
1085
for contained, unit in machine.get('containers', {}).items():
1086
yield contained, unit
1088
def iter_new_machines(self, old_status):
1089
for machine, data in self.iter_machines():
1090
if machine in old_status.status['machines']:
1094
def iter_units(self):
1095
for service_name, service in sorted(self.status['services'].items()):
1096
for unit_name, unit in sorted(service.get('units', {}).items()):
1097
yield unit_name, unit
1098
subordinates = unit.get('subordinates', ())
1099
for sub_name in sorted(subordinates):
1100
yield sub_name, subordinates[sub_name]
1102
def agent_items(self):
1103
for machine_name, machine in self.iter_machines(containers=True):
1104
yield machine_name, machine
1105
for unit_name, unit in self.iter_units():
1106
yield unit_name, unit
1108
def agent_states(self):
1109
"""Map agent states to the units and machines in those states."""
1110
states = defaultdict(list)
1111
for item_name, item in self.agent_items():
1112
states[item.get('agent-state', 'no-agent')].append(item_name)
1115
def check_agents_started(self, environment_name=None):
1116
"""Check whether all agents are in the 'started' state.
1118
If not, return agent_states output. If so, return None.
1119
If an error is encountered for an agent, raise ErroredUnit
1121
bad_state_info = re.compile(
1122
'(.*error|^(cannot set up groups|cannot run instance)).*')
1123
for item_name, item in self.agent_items():
1124
state_info = item.get('agent-state-info', '')
1125
if bad_state_info.match(state_info):
1126
raise ErroredUnit(item_name, state_info)
1127
states = self.agent_states()
1128
if states.keys() == ['started']:
1130
for state, entries in states.items():
1131
if 'error' in state:
1132
raise ErroredUnit(entries[0], state)
1135
def get_service_count(self):
1136
return len(self.status.get('services', {}))
1138
def get_service_unit_count(self, service):
1140
self.status.get('services', {}).get(service, {}).get('units', {}))
1142
def get_agent_versions(self):
1143
versions = defaultdict(set)
1144
for item_name, item in self.agent_items():
1145
versions[item.get('agent-version', 'unknown')].add(item_name)
1148
def get_instance_id(self, machine_id):
1149
return self.status['machines'][machine_id]['instance-id']
1151
def get_unit(self, unit_name):
1152
"""Return metadata about a unit."""
1153
for service in sorted(self.status['services'].values()):
1154
if unit_name in service.get('units', {}):
1155
return service['units'][unit_name]
1156
raise KeyError(unit_name)
1158
def service_subordinate_units(self, service_name):
1159
"""Return subordinate metadata for a service_name."""
1160
services = self.status.get('services', {})
1161
if service_name in services:
1162
for unit in sorted(services[service_name].get(
1163
'units', {}).values()):
1164
for sub_name, sub in unit.get('subordinates', {}).items():
1167
def get_open_ports(self, unit_name):
1168
"""List the open ports for the specified unit.
1170
If no ports are listed for the unit, the empty list is returned.
1172
return self.get_unit(unit_name).get('open-ports', [])
1175
class SimpleEnvironment:
1177
def __init__(self, environment, config=None, juju_home=None):
1178
self.environment = environment
1179
self.config = config
1180
self.juju_home = juju_home
1181
if self.config is not None:
1182
self.local = bool(self.config.get('type') == 'local')
1184
self.local and bool(self.config.get('container') == 'kvm'))
1185
self.hpcloud = bool(
1186
'hpcloudsvc' in self.config.get('auth-url', ''))
1187
self.maas = bool(self.config.get('type') == 'maas')
1188
self.joyent = bool(self.config.get('type') == 'joyent')
1192
self.hpcloud = False
1196
def __eq__(self, other):
1197
if type(self) != type(other):
1199
if self.environment != other.environment:
1201
if self.config != other.config:
1203
if self.local != other.local:
1205
if self.hpcloud != other.hpcloud:
1207
if self.maas != other.maas:
1211
def __ne__(self, other):
1212
return not self == other
1215
def from_config(cls, name):
1216
config, selected = get_selected_environment(name)
1219
return cls(name, config)
1221
def needs_sudo(self):
3053
1225
class GroupReporter: