268
82
return subprocess.check_output(('which', 'juju')).rstrip('\n')
271
def by_version(cls, env, juju_path=None, debug=False):
272
version = cls.get_version(juju_path)
273
if juju_path is None:
274
full_path = cls.get_full_path()
276
full_path = os.path.abspath(juju_path)
86
version = cls.get_version()
87
full_path = cls.get_full_path()
277
88
if version.startswith('1.16'):
278
89
raise Exception('Unsupported juju: %s' % version)
279
elif re.match('^1\.22[.-]', version):
280
client_class = EnvJujuClient22
281
elif re.match('^1\.24[.-]', version):
282
client_class = EnvJujuClient24
283
elif re.match('^1\.25[.-]', version):
284
client_class = EnvJujuClient25
285
elif re.match('^1\.26[.-]', version):
286
client_class = EnvJujuClient26
287
elif re.match('^1\.', version):
288
client_class = EnvJujuClient1X
289
elif re.match('^2\.0-alpha1', version):
290
client_class = EnvJujuClient2A1
291
elif re.match('^2\.0-alpha2', version):
292
client_class = EnvJujuClient2A2
293
elif re.match('^2\.0-(alpha3|beta[12])', version):
294
client_class = EnvJujuClient2B2
296
client_class = EnvJujuClient
297
return client_class(env, version, full_path, debug=debug)
299
def clone(self, env=None, version=None, full_path=None, debug=None,
301
"""Create a clone of this EnvJujuClient.
303
By default, the class, environment, version, full_path, and debug
304
settings will match the original, but each can be overridden.
309
version = self.version
310
if full_path is None:
311
full_path = self.full_path
316
other = cls(env, version, full_path, debug=debug)
317
other.feature_flags.update(
318
self.feature_flags.intersection(other.used_feature_flags))
321
def get_cache_path(self):
322
return get_cache_path(self.env.juju_home, models=True)
324
def _full_args(self, command, sudo, args,
325
timeout=None, include_e=True, admin=False):
91
return JujuClientDevel(version, full_path)
93
def _full_args(self, environment, command, sudo, args, timeout=None):
326
94
# sudo is not needed for devel releases.
328
e_arg = ('-m', self.get_admin_model_name())
329
elif self.env is None or not include_e:
332
e_arg = ('-m', self.env.environment)
95
e_arg = () if environment is None else ('-e', environment.environment)
333
96
if timeout is None:
336
prefix = get_timeout_prefix(timeout, self._timeout_path)
99
prefix = ('timeout', '%.2fs' % timeout)
337
100
logging = '--debug' if self.debug else '--show-log'
339
# If args is a string, make it a tuple. This makes writing commands
340
# with one argument a bit nicer.
341
if isinstance(args, basestring):
343
# we split the command here so that the caller can control where the -e
344
# <env> flag goes. Everything in the command string is put before the
346
command = command.split()
347
return prefix + ('juju', logging,) + tuple(command) + e_arg + args
351
if not isinstance(env, JujuData) and isinstance(env,
353
# FIXME: JujuData should be used from the start.
354
env = JujuData.from_env(env)
357
def __init__(self, env, version, full_path, juju_home=None, debug=False):
358
self.env = self._get_env(env)
359
self.version = version
360
self.full_path = full_path
362
self.feature_flags = set()
364
if juju_home is None:
365
if env.juju_home is None:
366
env.juju_home = get_juju_home()
368
env.juju_home = juju_home
369
self.juju_timings = {}
370
self._timeout_path = get_timeout_path()
372
def _shell_environ(self):
373
"""Generate a suitable shell environment.
375
Juju's directory must be in the PATH to support plugins.
377
env = dict(os.environ)
378
if self.full_path is not None:
379
env['PATH'] = '{}{}{}'.format(os.path.dirname(self.full_path),
380
os.pathsep, env['PATH'])
381
flags = self.feature_flags.intersection(self.used_feature_flags)
383
env[JUJU_DEV_FEATURE_FLAGS] = ','.join(sorted(flags))
384
env['JUJU_DATA'] = self.env.juju_home
387
def add_ssh_machines(self, machines):
388
for count, machine in enumerate(machines):
390
self.juju('add-machine', ('ssh:' + machine,))
391
except subprocess.CalledProcessError:
394
logging.warning('add-machine failed. Will retry.')
396
self.juju('add-machine', ('ssh:' + machine,))
399
def get_cloud_region(cloud, region):
402
return '{}/{}'.format(cloud, region)
404
def get_bootstrap_args(self, upload_tools, config_filename,
405
bootstrap_series=None):
406
"""Return the bootstrap arguments for the substrate."""
408
constraints = 'mem=2G arch=amd64'
409
elif self.env.joyent:
410
# Only accept kvm packages by requiring >1 cpu core, see lp:1446264
411
constraints = 'mem=2G cpu-cores=1'
413
constraints = 'mem=2G'
414
cloud_region = self.get_cloud_region(self.env.get_cloud(),
415
self.env.get_region())
416
args = ['--constraints', constraints, self.env.environment,
417
cloud_region, '--config', config_filename,
418
'--default-model', self.env.environment]
420
args.insert(0, '--upload-tools')
422
args.extend(['--agent-version', self.get_matching_agent_version()])
424
if bootstrap_series is not None:
425
args.extend(['--bootstrap-series', bootstrap_series])
429
def _bootstrap_config(self):
430
config_dict = make_safe_config(self)
431
# Strip unneeded variables.
432
config_dict = dict((k, v) for k, v in config_dict.items() if k not in {
436
'application-password',
455
'storage-account-name',
462
with temp_yaml_file(config_dict) as config_filename:
463
yield config_filename
465
def _check_bootstrap(self):
466
if self.env.environment != self.env.controller.name:
467
raise AssertionError(
468
'Controller and environment names should not vary (yet)')
470
def bootstrap(self, upload_tools=False, bootstrap_series=None):
471
"""Bootstrap a controller."""
472
self._check_bootstrap()
473
with self._bootstrap_config() as config_filename:
474
args = self.get_bootstrap_args(
475
upload_tools, config_filename, bootstrap_series)
476
self.juju('bootstrap', args, include_e=False)
479
def bootstrap_async(self, upload_tools=False, bootstrap_series=None):
480
self._check_bootstrap()
481
with self._bootstrap_config() as config_filename:
482
args = self.get_bootstrap_args(
483
upload_tools, config_filename, bootstrap_series)
484
with self.juju_async('bootstrap', args, include_e=False):
486
log.info('Waiting for bootstrap of {}.'.format(
487
self.env.environment))
489
def create_environment(self, controller_client, config_file):
490
controller_client.controller_juju('create-model', (
491
self.env.environment, '--config', config_file))
493
def destroy_model(self):
494
exit_status = self.juju(
495
'destroy-model', (self.env.environment, '-y',),
496
include_e=False, timeout=timedelta(minutes=10).total_seconds())
499
def kill_controller(self):
500
"""Kill a controller and its environments."""
501
seen_cmd = self.get_jes_command()
101
return prefix + ('juju', logging, command,) + e_arg + args
103
def bootstrap(self, environment):
104
"""Bootstrap, using sudo if necessary."""
105
if environment.hpcloud:
106
constraints = 'mem=2G'
108
constraints = 'mem=2G'
109
self.juju(environment, 'bootstrap', ('--constraints', constraints),
110
environment.needs_sudo())
112
def destroy_environment(self, environment):
503
_jes_cmds[seen_cmd]['kill'], (self.env.controller.name, '-y'),
504
include_e=False, check=False, timeout=600)
506
def get_juju_output(self, command, *args, **kwargs):
507
"""Call a juju command and return the output.
509
Sub process will be called as 'juju <command> <args> <kwargs>'. Note
510
that <command> may be a space delimited list of arguments. The -e
511
<environment> flag will be placed after <command> and before args.
513
args = self._full_args(command, False, args,
514
timeout=kwargs.get('timeout'),
515
include_e=kwargs.get('include_e', True),
516
admin=kwargs.get('admin', False))
517
env = self._shell_environ()
519
# Mutate os.environ instead of supplying env parameter so
520
# Windows can search env['PATH']
521
with scoped_environ(env):
522
proc = subprocess.Popen(
523
args, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
524
stderr=subprocess.PIPE)
525
sub_output, sub_error = proc.communicate()
526
log.debug(sub_output)
527
if proc.returncode != 0:
529
e = subprocess.CalledProcessError(
530
proc.returncode, args, sub_output)
533
'Unable to connect to environment' in sub_error or
534
'MissingOrIncorrectVersionHeader' in sub_error or
535
'307: Temporary Redirect' in sub_error):
114
None, 'destroy-environment',
115
(environment.environment, '--force', '-y'),
116
environment.needs_sudo(), check=False)
118
def get_juju_output(self, environment, command, *args, **kwargs):
119
args = self._full_args(environment, command, False, args,
120
timeout=kwargs.get('timeout'))
121
with tempfile.TemporaryFile() as stderr:
123
return subprocess.check_output(args, stderr=stderr)
124
except subprocess.CalledProcessError as e:
126
e.stderr = stderr.read()
127
if ('Unable to connect to environment' in e.stderr
128
or 'MissingOrIncorrectVersionHeader' in e.stderr
129
or '307: Temporary Redirect' in e.stderr):
536
130
raise CannotConnectEnv(e)
540
def show_status(self):
541
"""Print the status to output."""
542
self.juju(self._show_status, ('--format', 'yaml'))
544
def get_status(self, timeout=60, raw=False, admin=False, *args):
131
print('!!! ' + e.stderr)
134
def get_status(self, environment, timeout=60):
545
135
"""Get the current status as a dict."""
546
# GZ 2015-12-16: Pass remaining timeout into get_juju_output call.
547
for ignored in until_timeout(timeout):
550
return self.get_juju_output(self._show_status, *args)
551
return Status.from_text(
552
self.get_juju_output(
553
self._show_status, '--format', 'yaml', admin=admin))
554
except subprocess.CalledProcessError:
557
'Timed out waiting for juju status to succeed')
560
def _dict_as_option_strings(options):
561
return tuple('{}={}'.format(*item) for item in options.items())
563
def set_config(self, service, options):
564
option_strings = self._dict_as_option_strings(options)
565
self.juju('set-config', (service,) + option_strings)
567
def get_config(self, service):
568
return yaml_loads(self.get_juju_output('get-config', service))
570
def get_service_config(self, service, timeout=60):
571
for ignored in until_timeout(timeout):
573
return self.get_config(service)
574
except subprocess.CalledProcessError:
577
'Timed out waiting for juju get %s' % (service))
579
def set_model_constraints(self, constraints):
580
constraint_strings = self._dict_as_option_strings(constraints)
581
return self.juju('set-model-constraints', constraint_strings)
583
def get_model_config(self):
584
"""Return the value of the environment's configured option."""
585
return yaml.safe_load(self.get_juju_output('get-model-config'))
587
def get_env_option(self, option):
588
"""Return the value of the environment's configured option."""
589
return self.get_juju_output('get-model-config', option)
591
def set_env_option(self, option, value):
136
for ignored in until_timeout(timeout):
138
return Status(yaml_loads(
139
self.get_juju_output(environment, 'status')))
140
except subprocess.CalledProcessError as e:
143
'Timed out waiting for juju status to succeed: %s' % e)
145
def get_env_option(self, environment, option):
146
"""Return the value of the environment's configured option."""
147
return self.get_juju_output(environment, 'get-env', option)
149
def set_env_option(self, environment, option, value):
592
150
"""Set the value of the option in the environment."""
593
151
option_value = "%s=%s" % (option, value)
594
return self.juju('set-model-config', (option_value,))
596
def set_testing_tools_metadata_url(self):
597
url = self.get_env_option('tools-metadata-url')
598
if 'testing' not in url:
599
testing_url = url.replace('/tools', '/testing/tools')
600
self.set_env_option('tools-metadata-url', testing_url)
602
def juju(self, command, args, sudo=False, check=True, include_e=True,
603
timeout=None, extra_env=None):
152
return self.juju(environment, 'set-env', (option_value,))
154
def juju(self, environment, command, args, sudo=False, check=True):
604
155
"""Run a command under juju for the current environment."""
605
args = self._full_args(command, sudo, args, include_e=include_e,
607
log.info(' '.join(args))
608
env = self._shell_environ()
609
if extra_env is not None:
610
env.update(extra_env)
156
args = self._full_args(environment, command, sudo, args)
157
print(' '.join(args))
612
call_func = subprocess.check_call
614
call_func = subprocess.call
615
start_time = time.time()
616
# Mutate os.environ instead of supplying env parameter so Windows can
618
with scoped_environ(env):
619
rval = call_func(args)
620
self.juju_timings.setdefault(args, []).append(
621
(time.time() - start_time))
624
def controller_juju(self, command, args):
625
args = ('-c', self.env.controller.name) + args
626
return self.juju(command, args, include_e=False)
628
def get_juju_timings(self):
629
stringified_timings = {}
630
for command, timings in self.juju_timings.items():
631
stringified_timings[' '.join(command)] = timings
632
return stringified_timings
635
def juju_async(self, command, args, include_e=True, timeout=None):
636
full_args = self._full_args(command, False, args, include_e=include_e,
638
log.info(' '.join(args))
639
env = self._shell_environ()
640
# Mutate os.environ instead of supplying env parameter so Windows can
642
with scoped_environ(env):
643
proc = subprocess.Popen(full_args)
645
retcode = proc.wait()
647
raise subprocess.CalledProcessError(retcode, full_args)
649
def deploy(self, charm, repository=None, to=None, series=None,
650
service=None, force=False):
653
args.extend(['--to', to])
654
if series is not None:
655
args.extend(['--series', series])
656
if service is not None:
657
args.extend([service])
659
args.extend(['--force'])
660
return self.juju('deploy', tuple(args))
662
def remove_service(self, service):
663
self.juju('remove-service', (service,))
665
def deploy_bundle(self, bundle, timeout=_DEFAULT_BUNDLE_TIMEOUT):
666
"""Deploy bundle using native juju 2.0 deploy command."""
667
self.juju('deploy', bundle, timeout=timeout)
669
def deployer(self, bundle, name=None, deploy_delay=10, timeout=3600):
670
"""deployer, using sudo if necessary."""
673
'--deploy-delay', str(deploy_delay),
674
'--timeout', str(timeout),
679
self.juju('deployer', args, self.env.needs_sudo())
681
def _get_substrate_constraints(self):
683
return 'mem=2G arch=amd64'
684
elif self.env.joyent:
685
# Only accept kvm packages by requiring >1 cpu core, see lp:1446264
686
return 'mem=2G cpu-cores=1'
690
def quickstart(self, bundle, upload_tools=False):
691
"""quickstart, using sudo if necessary."""
693
constraints = 'mem=2G arch=amd64'
695
constraints = 'mem=2G'
696
args = ('--constraints', constraints)
698
args = ('--upload-tools',) + args
699
args = args + ('--no-browser', bundle,)
700
self.juju('quickstart', args, self.env.needs_sudo(),
701
extra_env={'JUJU': self.full_path})
703
def status_until(self, timeout, start=None):
704
"""Call and yield status until the timeout is reached.
706
Status will always be yielded once before checking the timeout.
708
This is intended for implementing things like wait_for_started.
710
:param timeout: The number of seconds to wait before timing out.
711
:param start: If supplied, the time to count from when determining
714
yield self.get_status()
715
for remaining in until_timeout(timeout, start=start):
716
yield self.get_status()
718
def _wait_for_status(self, reporter, translate, exc_type=StatusNotMet,
719
timeout=1200, start=None):
720
"""Wait till status reaches an expected state with pretty reporting.
722
Always tries to get status at least once. Each status call has an
723
internal timeout of 60 seconds. This is independent of the timeout for
724
the whole wait, note this means this function may be overrun.
726
:param reporter: A GroupReporter instance for output.
727
:param translate: A callable that takes status to make states dict.
728
:param exc_type: Optional StatusNotMet subclass to raise on timeout.
729
:param timeout: Optional number of seconds to wait before timing out.
730
:param start: Optional time to count from when determining timeout.
734
for _ in chain([None], until_timeout(timeout, start=start)):
736
status = self.get_status()
737
except CannotConnectEnv:
738
log.info('Suppressing "Unable to connect to environment"')
740
states = translate(status)
743
reporter.update(states)
745
if status is not None:
746
log.error(status.status_text)
747
raise exc_type(self.env.environment, status)
752
def wait_for_started(self, timeout=1200, start=None):
753
"""Wait until all unit/machine agents are 'started'."""
754
reporter = GroupReporter(sys.stdout, 'started')
755
return self._wait_for_status(
756
reporter, Status.check_agents_started, AgentsNotStarted,
757
timeout=timeout, start=start)
759
def wait_for_subordinate_units(self, service, unit_prefix, timeout=1200,
761
"""Wait until all service units have a started subordinate with
763
def status_to_subordinate_states(status):
764
service_unit_count = status.get_service_unit_count(service)
765
subordinate_unit_count = 0
766
unit_states = defaultdict(list)
767
for name, unit in status.service_subordinate_units(service):
768
if name.startswith(unit_prefix + '/'):
769
subordinate_unit_count += 1
770
unit_states[coalesce_agent_status(unit)].append(name)
771
if (subordinate_unit_count == service_unit_count and
772
set(unit_states.keys()).issubset(AGENTS_READY)):
775
reporter = GroupReporter(sys.stdout, 'started')
776
self._wait_for_status(
777
reporter, status_to_subordinate_states, AgentsNotStarted,
778
timeout=timeout, start=start)
780
def wait_for_version(self, version, timeout=300, start=None):
781
def status_to_version(status):
782
versions = status.get_agent_versions()
783
if versions.keys() == [version]:
786
reporter = GroupReporter(sys.stdout, version)
787
self._wait_for_status(reporter, status_to_version, VersionsNotUpdated,
788
timeout=timeout, start=start)
790
def list_models(self):
791
"""List the models registered with the current controller."""
792
self.controller_juju('list-models', ())
794
def get_models(self):
795
"""return a models dict with a 'models': [] key-value pair."""
796
output = self.get_juju_output(
797
'list-models', '-c', self.env.environment, '--format', 'yaml',
799
models = yaml_loads(output)
802
def _get_models(self):
803
"""return a list of model dicts."""
804
return self.get_models()['models']
806
def iter_model_clients(self):
807
"""Iterate through all the models that share this model's controller.
809
Works only if JES is enabled.
811
models = self._get_models()
815
yield self._acquire_model_client(model['name'])
817
def get_admin_model_name(self):
818
"""Return the name of the 'admin' model.
820
Return the name of the environment when an 'admin' model does
825
def _acquire_model_client(self, name):
826
"""Get a client for a model with the supplied name.
828
If the name matches self, self is used. Otherwise, a clone is used.
830
if name == self.env.environment:
833
env = self.env.clone(model_name=name)
834
return self.clone(env=env)
836
def get_admin_client(self):
837
"""Return a client for the admin model. May return self.
839
This may be inaccurate for models created using create_environment
840
rather than bootstrap.
842
return self._acquire_model_client(self.get_admin_model_name())
844
def list_controllers(self):
845
"""List the controllers."""
846
self.juju('list-controllers', (), include_e=False)
848
def get_controller_endpoint(self):
849
"""Return the address of the controller leader."""
850
controller = self.env.controller.name
851
output = self.get_juju_output(
852
'show-controller', controller, include_e=False)
853
info = yaml_loads(output)
854
endpoint = info[controller]['details']['api-endpoints'][0]
855
address, port = split_address_port(endpoint)
858
def get_controller_members(self):
859
"""Return a list of Machines that are members of the controller.
861
The first machine in the list is the leader. the remaining machines
862
are followers in a HA relationship.
865
status = self.get_status()
866
for machine_id, machine in status.iter_machines():
867
if self.get_controller_member_status(machine):
868
members.append(Machine(machine_id, machine))
869
if len(members) <= 1:
871
# Search for the leader and make it the first in the list.
872
# If the endpoint address is not the same as the leader's dns_name,
873
# the members are return in the order they were discovered.
874
endpoint = self.get_controller_endpoint()
875
log.debug('Controller endpoint is at {}'.format(endpoint))
876
members.sort(key=lambda m: m.info.get('dns-name') != endpoint)
879
def get_controller_leader(self):
880
"""Return the controller leader Machine."""
881
controller_members = self.get_controller_members()
882
return controller_members[0]
885
def get_controller_member_status(info_dict):
886
"""Return the controller-member-status of the machine if it exists."""
887
return info_dict.get('controller-member-status')
889
def wait_for_ha(self, timeout=1200):
890
desired_state = 'has-vote'
891
reporter = GroupReporter(sys.stdout, desired_state)
893
for remaining in until_timeout(timeout):
894
status = self.get_status(admin=True)
896
for machine, info in status.iter_machines():
897
status = self.get_controller_member_status(info)
900
states.setdefault(status, []).append(machine)
901
if states.keys() == [desired_state]:
902
if len(states.get(desired_state, [])) >= 3:
903
# XXX sinzui 2014-12-04: bug 1399277 happens because
904
# juju claims HA is ready when the monogo replica sets
905
# are not. Juju is not fully usable. The replica set
906
# lag might be 5 minutes.
909
reporter.update(states)
911
raise Exception('Timed out waiting for voting to be enabled.')
915
def wait_for_deploy_started(self, service_count=1, timeout=1200):
916
"""Wait until service_count services are 'started'.
918
:param service_count: The number of services for which to wait.
919
:param timeout: The number of seconds to wait.
921
for remaining in until_timeout(timeout):
922
status = self.get_status()
923
if status.get_service_count() >= service_count:
926
raise Exception('Timed out waiting for services to start.')
928
def wait_for_workloads(self, timeout=600, start=None):
929
"""Wait until all unit workloads are in a ready state."""
930
def status_to_workloads(status):
931
unit_states = defaultdict(list)
932
for name, unit in status.iter_units():
933
workload = unit.get('workload-status')
934
if workload is not None:
935
state = workload['current']
938
unit_states[state].append(name)
939
if set(('active', 'unknown')).issuperset(unit_states):
941
unit_states.pop('unknown', None)
943
reporter = GroupReporter(sys.stdout, 'active')
944
self._wait_for_status(reporter, status_to_workloads, WorkloadsNotReady,
945
timeout=timeout, start=start)
947
def wait_for(self, thing, search_type, timeout=300):
948
""" Wait for a something (thing) matching none/all/some machines.
951
wait_for('containers', 'all')
952
This will wait for a container to appear on all machines.
954
wait_for('machines-not-0', 'none')
955
This will wait for all machines other than 0 to be removed.
957
:param thing: string, either 'containers' or 'not-machine-0'
958
:param search_type: string containing none, some or all
959
:param timeout: number of seconds to wait for condition to be true.
963
for status in self.status_until(timeout):
967
for machine, details in status.status['machines'].iteritems():
968
if thing == 'containers':
969
if 'containers' in details:
974
elif thing == 'machines-not-0':
981
raise ValueError("Unrecognised thing to wait for: %s",
984
if search_type == 'none':
987
elif search_type == 'some':
990
elif search_type == 'all':
994
raise Exception("Timed out waiting for %s" % thing)
996
def get_matching_agent_version(self, no_build=False):
997
# strip the series and srch from the built version.
998
version_parts = self.version.split('-')
999
if len(version_parts) == 4:
1000
version_number = '-'.join(version_parts[0:2])
1002
version_number = version_parts[0]
1003
if not no_build and self.env.local:
1004
version_number += '.1'
1005
return version_number
1007
def upgrade_juju(self, force_version=True):
1010
version = self.get_matching_agent_version(no_build=True)
1011
args += ('--version', version)
1013
args += ('--upload-tools',)
1014
self.juju('upgrade-juju', args)
1016
def upgrade_mongo(self):
1017
self.juju('upgrade-mongo', ())
1020
environ = self._shell_environ()
1022
# Mutate os.environ instead of supplying env parameter so Windows
1023
# can search env['PATH']
1024
with scoped_environ(environ):
1025
args = self._full_args(
1026
'create-backup', False, (), include_e=True)
1027
log.info(' '.join(args))
1028
output = subprocess.check_output(args)
1029
except subprocess.CalledProcessError as e:
1033
backup_file_pattern = re.compile('(juju-backup-[0-9-]+\.(t|tar.)gz)')
1034
match = backup_file_pattern.search(output)
1036
raise Exception("The backup file was not found in output: %s" %
1038
backup_file_name = match.group(1)
1039
backup_file_path = os.path.abspath(backup_file_name)
1040
log.info("State-Server backup at %s", backup_file_path)
1041
return backup_file_path
1043
def restore_backup(self, backup_file):
1044
return self.get_juju_output('restore-backup', '-b', '--constraints',
1045
'mem=2G', '--file', backup_file)
1047
def restore_backup_async(self, backup_file):
1048
return self.juju_async('restore-backup', ('-b', '--constraints',
1049
'mem=2G', '--file', backup_file))
1051
def enable_ha(self):
1052
self.juju('enable-ha', ('-n', '3'))
1054
def action_fetch(self, id, action=None, timeout="1m"):
1055
"""Fetches the results of the action with the given id.
1057
Will wait for up to 1 minute for the action results.
1058
The action name here is just used for an more informational error in
1059
cases where it's available.
1060
Returns the yaml output of the fetched action.
1062
out = self.get_juju_output("show-action-output", id, "--wait", timeout)
1063
status = yaml_loads(out)["status"]
1064
if status != "completed":
1066
if action is not None:
1069
"timed out waiting for action%s to complete during fetch" %
1073
def action_do(self, unit, action, *args):
1074
"""Performs the given action on the given unit.
1076
Action params should be given as args in the form foo=bar.
1077
Returns the id of the queued action.
1079
args = (unit, action) + args
1081
output = self.get_juju_output("run-action", *args)
1082
action_id_pattern = re.compile(
1083
'Action queued with id: ([a-f0-9\-]{36})')
1084
match = action_id_pattern.search(output)
1086
raise Exception("Action id not found in output: %s" %
1088
return match.group(1)
1090
def action_do_fetch(self, unit, action, timeout="1m", *args):
1091
"""Performs given action on given unit and waits for the results.
1093
Action params should be given as args in the form foo=bar.
1094
Returns the yaml output of the action.
1096
id = self.action_do(unit, action, *args)
1097
return self.action_fetch(id, action, timeout)
1099
def list_space(self):
1100
return yaml.safe_load(self.get_juju_output('list-space'))
1102
def add_space(self, space):
1103
self.juju('add-space', (space),)
1105
def add_subnet(self, subnet, space):
1106
self.juju('add-subnet', (subnet, space))
1109
class EnvJujuClient2B2(EnvJujuClient):
1111
def get_bootstrap_args(self, upload_tools, config_filename,
1112
bootstrap_series=None):
1113
"""Return the bootstrap arguments for the substrate."""
1115
constraints = 'mem=2G arch=amd64'
1116
elif self.env.joyent:
1117
# Only accept kvm packages by requiring >1 cpu core, see lp:1446264
1118
constraints = 'mem=2G cpu-cores=1'
1120
constraints = 'mem=2G'
1121
cloud_region = self.get_cloud_region(self.env.get_cloud(),
1122
self.env.get_region())
1123
args = ['--constraints', constraints, self.env.environment,
1124
cloud_region, '--config', config_filename]
1126
args.insert(0, '--upload-tools')
1128
args.extend(['--agent-version', self.get_matching_agent_version()])
1130
if bootstrap_series is not None:
1131
args.extend(['--bootstrap-series', bootstrap_series])
1134
def get_admin_client(self):
1135
"""Return a client for the admin model. May return self."""
1138
def get_admin_model_name(self):
1139
"""Return the name of the 'admin' model.
1141
Return the name of the environment when an 'admin' model does
1144
models = self.get_models()
1145
# The dict can be empty because 1.x does not support the models.
1146
# This is an ambiguous case for the jes feature flag which supports
1147
# multiple models, but none is named 'admin' by default. Since the
1148
# jes case also uses '-e' for models, the env is the admin model.
1149
for model in models.get('models', []):
1150
if 'admin' in model['name']:
1152
return self.env.environment
1155
class EnvJujuClient2A2(EnvJujuClient2B2):
1156
"""Drives Juju 2.0-alpha2 clients."""
1159
def _get_env(cls, env):
1160
if isinstance(env, JujuData):
1162
'JujuData cannot be used with {}'.format(cls.__name__))
1165
def _shell_environ(self):
1166
"""Generate a suitable shell environment.
1168
For 2.0-alpha2 set both JUJU_HOME and JUJU_DATA.
1170
env = super(EnvJujuClient2A2, self)._shell_environ()
1171
env['JUJU_HOME'] = self.env.juju_home
1174
def bootstrap(self, upload_tools=False, bootstrap_series=None):
1175
"""Bootstrap a controller."""
1176
self._check_bootstrap()
1177
args = self.get_bootstrap_args(upload_tools, bootstrap_series)
1178
self.juju('bootstrap', args, self.env.needs_sudo())
1181
def bootstrap_async(self, upload_tools=False):
1182
self._check_bootstrap()
1183
args = self.get_bootstrap_args(upload_tools)
1184
with self.juju_async('bootstrap', args):
1186
log.info('Waiting for bootstrap of {}.'.format(
1187
self.env.environment))
1189
def get_bootstrap_args(self, upload_tools, bootstrap_series=None):
1190
"""Return the bootstrap arguments for the substrate."""
1191
constraints = self._get_substrate_constraints()
1192
args = ('--constraints', constraints,
1193
'--agent-version', self.get_matching_agent_version())
1195
args = ('--upload-tools',) + args
1196
if bootstrap_series is not None:
1197
args = args + ('--bootstrap-series', bootstrap_series)
1200
def deploy(self, charm, repository=None, to=None, series=None,
1201
service=None, force=False):
1203
if repository is not None:
1204
args.extend(['--repository', repository])
1206
args.extend(['--to', to])
1207
if service is not None:
1208
args.extend([service])
1209
return self.juju('deploy', tuple(args))
1212
class EnvJujuClient2A1(EnvJujuClient2A2):
1213
"""Drives Juju 2.0-alpha1 clients."""
1215
_show_status = 'status'
1217
def get_cache_path(self):
1218
return get_cache_path(self.env.juju_home, models=False)
1220
def _full_args(self, command, sudo, args,
1221
timeout=None, include_e=True, admin=False):
1222
# sudo is not needed for devel releases.
1223
# admin is ignored. only environment exists.
1224
if self.env is None or not include_e:
1227
e_arg = ('-e', self.env.environment)
1231
prefix = get_timeout_prefix(timeout, self._timeout_path)
1232
logging = '--debug' if self.debug else '--show-log'
1234
# If args is a string, make it a tuple. This makes writing commands
1235
# with one argument a bit nicer.
1236
if isinstance(args, basestring):
1238
# we split the command here so that the caller can control where the -e
1239
# <env> flag goes. Everything in the command string is put before the
1241
command = command.split()
1242
return prefix + ('juju', logging,) + tuple(command) + e_arg + args
1244
def _shell_environ(self):
1245
"""Generate a suitable shell environment.
1247
For 2.0-alpha1 and earlier set only JUJU_HOME and not JUJU_DATA.
1249
env = super(EnvJujuClient2A1, self)._shell_environ()
1250
env['JUJU_HOME'] = self.env.juju_home
1251
del env['JUJU_DATA']
1254
def remove_service(self, service):
1255
self.juju('destroy-service', (service,))
1258
environ = self._shell_environ()
1259
# juju-backup does not support the -e flag.
1260
environ['JUJU_ENV'] = self.env.environment
1262
# Mutate os.environ instead of supplying env parameter so Windows
1263
# can search env['PATH']
1264
with scoped_environ(environ):
1265
args = ['juju', 'backup']
1266
log.info(' '.join(args))
1267
output = subprocess.check_output(args)
1268
except subprocess.CalledProcessError as e:
1272
backup_file_pattern = re.compile('(juju-backup-[0-9-]+\.(t|tar.)gz)')
1273
match = backup_file_pattern.search(output)
1275
raise Exception("The backup file was not found in output: %s" %
1277
backup_file_name = match.group(1)
1278
backup_file_path = os.path.abspath(backup_file_name)
1279
log.info("State-Server backup at %s", backup_file_path)
1280
return backup_file_path
1282
def restore_backup(self, backup_file):
1283
return self.get_juju_output('restore', '--constraints', 'mem=2G',
1286
def restore_backup_async(self, backup_file):
1287
return self.juju_async('restore', ('--constraints', 'mem=2G',
1290
def enable_ha(self):
1291
self.juju('ensure-availability', ('-n', '3'))
1293
def list_models(self):
1294
"""List the models registered with the current controller."""
1295
log.info('The model is environment {}'.format(self.env.environment))
1297
def get_models(self):
1298
"""return a models dict with a 'models': [] key-value pair."""
1301
def _get_models(self):
1302
"""return a list of model dicts."""
1303
# In 2.0-alpha1, 'list-models' produced a yaml list rather than a
1304
# dict, but the command and parsing are the same.
1305
return super(EnvJujuClient2A1, self).get_models()
1307
def list_controllers(self):
1308
"""List the controllers."""
1310
'The controller is environment {}'.format(self.env.environment))
1313
def get_controller_member_status(info_dict):
1314
return info_dict.get('state-server-member-status')
1316
def action_fetch(self, id, action=None, timeout="1m"):
1317
"""Fetches the results of the action with the given id.
1319
Will wait for up to 1 minute for the action results.
1320
The action name here is just used for an more informational error in
1321
cases where it's available.
1322
Returns the yaml output of the fetched action.
1324
# the command has to be "action fetch" so that the -e <env> args are
1325
# placed after "fetch", since that's where action requires them to be.
1326
out = self.get_juju_output("action fetch", id, "--wait", timeout)
1327
status = yaml_loads(out)["status"]
1328
if status != "completed":
1330
if action is not None:
1333
"timed out waiting for action%s to complete during fetch" %
1337
def action_do(self, unit, action, *args):
1338
"""Performs the given action on the given unit.
1340
Action params should be given as args in the form foo=bar.
1341
Returns the id of the queued action.
1343
args = (unit, action) + args
1345
# the command has to be "action do" so that the -e <env> args are
1346
# placed after "do", since that's where action requires them to be.
1347
output = self.get_juju_output("action do", *args)
1348
action_id_pattern = re.compile(
1349
'Action queued with id: ([a-f0-9\-]{36})')
1350
match = action_id_pattern.search(output)
1352
raise Exception("Action id not found in output: %s" %
1354
return match.group(1)
1356
def list_space(self):
1357
return yaml.safe_load(self.get_juju_output('space list'))
1359
def add_space(self, space):
1360
self.juju('space create', (space),)
1362
def add_subnet(self, subnet, space):
1363
self.juju('subnet add', (subnet, space))
1365
def set_model_constraints(self, constraints):
1366
constraint_strings = self._dict_as_option_strings(constraints)
1367
return self.juju('set-constraints', constraint_strings)
1369
def set_config(self, service, options):
1370
option_strings = ['{}={}'.format(*item) for item in options.items()]
1371
self.juju('set', (service,) + tuple(option_strings))
1373
def get_config(self, service):
1374
return yaml_loads(self.get_juju_output('get', service))
1376
def get_model_config(self):
1377
"""Return the value of the environment's configured option."""
1378
return yaml.safe_load(self.get_juju_output('get-env'))
1380
def get_env_option(self, option):
1381
"""Return the value of the environment's configured option."""
1382
return self.get_juju_output('get-env', option)
1384
def set_env_option(self, option, value):
1385
"""Set the value of the option in the environment."""
1386
option_value = "%s=%s" % (option, value)
1387
return self.juju('set-env', (option_value,))
1390
class EnvJujuClient1X(EnvJujuClient2A1):
1391
"""Base for all 1.x client drivers."""
1393
# The environments.yaml options that are replaced by bootstrap options.
1394
# For Juju 1.x, no bootstrap options are used.
1395
bootstrap_replaces = frozenset()
1397
def get_bootstrap_args(self, upload_tools, bootstrap_series=None):
1398
"""Return the bootstrap arguments for the substrate."""
1399
constraints = self._get_substrate_constraints()
1400
args = ('--constraints', constraints)
1402
args = ('--upload-tools',) + args
1403
if bootstrap_series is not None:
1404
env_val = self.env.config.get('default-series')
1405
if bootstrap_series != env_val:
1406
raise BootstrapMismatch(
1407
'bootstrap-series', bootstrap_series, 'default-series',
1411
def get_jes_command(self):
1412
"""Return the JES command to destroy a controller.
1414
Juju 2.x has 'kill-controller'.
1415
Some intermediate versions had 'controller kill'.
1416
Juju 1.25 has 'system kill' when the jes feature flag is set.
1418
:raises: JESNotSupported when the version of Juju does not expose
1420
:return: The JES command.
1422
commands = self.get_juju_output('help', 'commands', include_e=False)
1423
for line in commands.splitlines():
1424
for cmd in _jes_cmds.keys():
1425
if line.startswith(cmd):
1427
raise JESNotSupported()
1429
def create_environment(self, controller_client, config_file):
1430
seen_cmd = self.get_jes_command()
1431
if seen_cmd == SYSTEM:
1432
controller_option = ('-s', controller_client.env.environment)
1434
controller_option = ('-c', controller_client.env.environment)
1435
self.juju(_jes_cmds[seen_cmd]['create'], controller_option + (
1436
self.env.environment, '--config', config_file), include_e=False)
1438
def destroy_model(self):
1439
"""With JES enabled, destroy-environment destroys the model."""
1440
self.destroy_environment(force=False)
1442
def destroy_environment(self, force=True, delete_jenv=False):
1444
force_arg = ('--force',)
1447
exit_status = self.juju(
1448
'destroy-environment',
1449
(self.env.environment,) + force_arg + ('-y',),
1450
self.env.needs_sudo(), check=False, include_e=False,
1451
timeout=timedelta(minutes=10).total_seconds())
1453
jenv_path = get_jenv_path(self.env.juju_home, self.env.environment)
1454
ensure_deleted(jenv_path)
1457
def _get_models(self):
1458
"""return a list of model dicts."""
1459
return yaml.safe_load(self.get_juju_output(
1460
'environments', '-s', self.env.environment, '--format', 'yaml',
1463
def deploy_bundle(self, bundle, timeout=_DEFAULT_BUNDLE_TIMEOUT):
1464
"""Deploy bundle using deployer for Juju 1.X version."""
1465
self.deployer(bundle, timeout=timeout)
1467
def get_controller_endpoint(self):
1468
"""Return the address of the state-server leader."""
1469
endpoint = self.get_juju_output('api-endpoints')
1470
address, port = split_address_port(endpoint)
1473
def upgrade_mongo(self):
1474
raise UpgradeMongoNotSupported()
1477
class EnvJujuClient22(EnvJujuClient1X):
1479
used_feature_flags = frozenset(['actions'])
1481
def __init__(self, *args, **kwargs):
1482
super(EnvJujuClient22, self).__init__(*args, **kwargs)
1483
self.feature_flags.add('actions')
1486
class EnvJujuClient26(EnvJujuClient1X):
1487
"""Drives Juju 2.6-series clients."""
1489
used_feature_flags = frozenset(['address-allocation', 'cloudsigma', 'jes'])
1491
def __init__(self, *args, **kwargs):
1492
super(EnvJujuClient26, self).__init__(*args, **kwargs)
1493
if self.env is None or self.env.config is None:
1495
if self.env.config.get('type') == 'cloudsigma':
1496
self.feature_flags.add('cloudsigma')
1498
def enable_jes(self):
1499
"""Enable JES if JES is optional.
1501
:raises: JESByDefault when JES is always enabled; Juju has the
1502
'destroy-controller' command.
1503
:raises: JESNotSupported when JES is not supported; Juju does not have
1504
the 'system kill' command when the JES feature flag is set.
1507
if 'jes' in self.feature_flags:
1509
if self.is_jes_enabled():
1510
raise JESByDefault()
1511
self.feature_flags.add('jes')
1512
if not self.is_jes_enabled():
1513
self.feature_flags.remove('jes')
1514
raise JESNotSupported()
1516
def disable_jes(self):
1517
if 'jes' in self.feature_flags:
1518
self.feature_flags.remove('jes')
1520
def enable_container_address_allocation(self):
1521
self.feature_flags.add('address-allocation')
1524
class EnvJujuClient25(EnvJujuClient26):
1525
"""Drives Juju 2.5-series clients."""
1528
class EnvJujuClient24(EnvJujuClient25):
1529
"""Similar to EnvJujuClient25, but lacking JES support."""
1531
used_feature_flags = frozenset(['cloudsigma'])
1533
def enable_jes(self):
1534
raise JESNotSupported()
1536
def add_ssh_machines(self, machines):
1537
for machine in machines:
1538
self.juju('add-machine', ('ssh:' + machine,))
1541
def get_local_root(juju_home, env):
1542
return os.path.join(juju_home, env.environment)
1545
def bootstrap_from_env(juju_home, client):
1546
with temp_bootstrap_env(juju_home, client):
1550
def quickstart_from_env(juju_home, client, bundle):
1551
with temp_bootstrap_env(juju_home, client):
1552
client.quickstart(bundle)
1556
def maybe_jes(client, jes_enabled, try_jes):
1557
"""If JES is desired and not enabled, try to enable it for this context.
1559
JES will be in its previous state after exiting this context.
1560
If jes_enabled is True or try_jes is False, the context is a no-op.
1561
If enable_jes() raises JESNotSupported, JES will not be enabled in the
1564
The with value is True if JES is enabled in the context.
1567
class JESUnwanted(Exception):
1568
"""Non-error. Used to avoid enabling JES if not wanted."""
1571
if not try_jes or jes_enabled:
1574
except (JESNotSupported, JESUnwanted):
1581
client.disable_jes()
1584
def tear_down(client, jes_enabled, try_jes=False):
1585
"""Tear down a JES or non-JES environment.
1587
JES environments are torn down via 'controller kill' or 'system kill',
1588
and non-JES environments are torn down via 'destroy-environment --force.'
1590
with maybe_jes(client, jes_enabled, try_jes) as jes_enabled:
1592
client.kill_controller()
1594
if client.destroy_environment(force=False) != 0:
1595
client.destroy_environment(force=True)
1598
def uniquify_local(env):
1599
"""Ensure that local environments have unique port settings.
1601
This allows local environments to be duplicated despite
1602
https://bugs.launchpad.net/bugs/1382131
1608
'state-port': 37017,
1609
'storage-port': 8040,
1610
'syslog-port': 6514,
1612
for key, default in port_defaults.items():
1613
env.config[key] = env.config.get(key, default) + 1
1616
def dump_environments_yaml(juju_home, config):
1617
environments_path = get_environments_path(juju_home)
1618
with open(environments_path, 'w') as config_file:
1619
yaml.safe_dump(config, config_file)
1623
def _temp_env(new_config, parent=None, set_home=True):
1624
"""Use the supplied config as juju environment.
1626
This is not a fully-formed version for bootstrapping. See
1629
with temp_dir(parent) as temp_juju_home:
1630
dump_environments_yaml(temp_juju_home, new_config)
1632
context = scoped_environ()
1637
os.environ['JUJU_HOME'] = temp_juju_home
1638
os.environ['JUJU_DATA'] = temp_juju_home
1639
yield temp_juju_home
1642
def jes_home_path(juju_home, dir_name):
1643
return os.path.join(juju_home, 'jes-homes', dir_name)
1646
def get_cache_path(juju_home, models=False):
1648
root = os.path.join(juju_home, 'models')
1650
root = os.path.join(juju_home, 'environments')
1651
return os.path.join(root, 'cache.yaml')
1654
def make_safe_config(client):
1655
config = dict(client.env.config)
1656
if 'agent-version' in client.bootstrap_replaces:
1657
config.pop('agent-version', None)
1659
config['agent-version'] = client.get_matching_agent_version()
1660
# AFAICT, we *always* want to set test-mode to True. If we ever find a
1661
# use-case where we don't, we can make this optional.
1662
config['test-mode'] = True
1663
# Explicitly set 'name', which Juju implicitly sets to env.environment to
1664
# ensure MAASAccount knows what the name will be.
1665
config['name'] = client.env.environment
1666
if config['type'] == 'local':
1667
config.setdefault('root-dir', get_local_root(client.env.juju_home,
1669
# MongoDB requires a lot of free disk space, and the only
1670
# visible error message is from "juju bootstrap":
1671
# "cannot initiate replication set" if disk space is low.
1672
# What "low" exactly means, is unclear, but 8GB should be
1674
ensure_dir(config['root-dir'])
1675
check_free_disk_space(config['root-dir'], 8000000, "MongoDB files")
1677
check_free_disk_space(
1678
"/var/lib/uvtool/libvirt/images", 2000000,
1681
check_free_disk_space(
1682
"/var/lib/lxc", 2000000, "LXC containers")
1687
def temp_bootstrap_env(juju_home, client, set_home=True, permanent=False):
1688
"""Create a temporary environment for bootstrapping.
1690
This involves creating a temporary juju home directory and returning its
1693
:param set_home: Set JUJU_HOME to match the temporary home in this
1694
context. If False, juju_home should be supplied to bootstrap.
1697
'environments': {client.env.environment: make_safe_config(client)}}
1698
# Always bootstrap a matching environment.
1699
jenv_path = get_jenv_path(juju_home, client.env.environment)
1701
context = client.env.make_jes_home(
1702
juju_home, client.env.environment, new_config)
1704
context = _temp_env(new_config, juju_home, set_home)
1705
with context as temp_juju_home:
1706
if os.path.lexists(jenv_path):
1707
raise Exception('%s already exists!' % jenv_path)
1708
new_jenv_path = get_jenv_path(temp_juju_home, client.env.environment)
1709
# Create a symlink to allow access while bootstrapping, and to reduce
1710
# races. Can't use a hard link because jenv doesn't exist until
1711
# partway through bootstrap.
1712
ensure_dir(os.path.join(juju_home, 'environments'))
1713
# Skip creating symlink where not supported (i.e. Windows).
1714
if not permanent and getattr(os, 'symlink', None) is not None:
1715
os.symlink(new_jenv_path, jenv_path)
1716
old_juju_home = client.env.juju_home
1717
client.env.juju_home = temp_juju_home
1719
yield temp_juju_home
1722
# replace symlink with file before deleting temp home.
1724
os.rename(new_jenv_path, jenv_path)
1725
except OSError as e:
1726
if e.errno != errno.ENOENT:
1728
# Remove dangling symlink
1730
os.unlink(jenv_path)
1731
except OSError as e:
1732
if e.errno != errno.ENOENT:
1734
client.env.juju_home = old_juju_home
1737
def get_machine_dns_name(client, machine, timeout=600):
1738
"""Wait for dns-name on a juju machine."""
1739
for status in client.status_until(timeout=timeout):
1741
return _dns_name_for_machine(status, machine)
1743
log.debug("No dns-name yet for machine %s", machine)
1746
def _dns_name_for_machine(status, machine):
1747
host = status.status['machines'][machine]['dns-name']
1748
if is_ipv6_address(host):
1749
log.warning("Selected IPv6 address for machine %s: %r", machine, host)
1755
def __init__(self, name):
160
return subprocess.check_call(args)
161
return subprocess.call(args)
1761
def __init__(self, status, status_text):
166
def __init__(self, status):
1762
167
self.status = status
1763
self.status_text = status_text
1766
def from_text(cls, text):
1767
status_yaml = yaml_loads(text)
1768
return cls(status_yaml, text)
1770
def iter_machines(self, containers=False, machines=True):
169
def iter_machines(self):
1771
170
for machine_name, machine in sorted(self.status['machines'].items()):
1773
yield machine_name, machine
1775
for contained, unit in machine.get('containers', {}).items():
1776
yield contained, unit
1778
def iter_new_machines(self, old_status):
1779
for machine, data in self.iter_machines():
1780
if machine in old_status.status['machines']:
1784
def iter_units(self):
1785
for service_name, service in sorted(self.status['services'].items()):
1786
for unit_name, unit in sorted(service.get('units', {}).items()):
171
yield machine_name, machine
173
def agent_items(self):
174
for result in self.iter_machines():
176
for service in sorted(self.status['services'].values()):
177
for unit_name, unit in service.get('units', {}).items():
1787
178
yield unit_name, unit
1788
subordinates = unit.get('subordinates', ())
1789
for sub_name in sorted(subordinates):
1790
yield sub_name, subordinates[sub_name]
1792
def agent_items(self):
1793
for machine_name, machine in self.iter_machines(containers=True):
1794
yield machine_name, machine
1795
for unit_name, unit in self.iter_units():
1796
yield unit_name, unit
1798
180
def agent_states(self):
1799
181
"""Map agent states to the units and machines in those states."""
1800
182
states = defaultdict(list)
1801
183
for item_name, item in self.agent_items():
1802
states[coalesce_agent_status(item)].append(item_name)
184
states[item.get('agent-state', 'no-agent')].append(item_name)
1805
def check_agents_started(self, environment_name=None):
187
def check_agents_started(self, environment_name):
1806
188
"""Check whether all agents are in the 'started' state.
1808
190
If not, return agent_states output. If so, return None.
1809
191
If an error is encountered for an agent, raise ErroredUnit
1811
bad_state_info = re.compile(
1812
'(.*error|^(cannot set up groups|cannot run instance)).*')
193
# Look for errors preventing an agent from being installed
1813
194
for item_name, item in self.agent_items():
1814
195
state_info = item.get('agent-state-info', '')
1815
if bad_state_info.match(state_info):
196
if 'error' in state_info:
1816
197
raise ErroredUnit(item_name, state_info)
1817
198
states = self.agent_states()
1818
if set(states.keys()).issubset(AGENTS_READY):
199
if states.keys() == ['started']:
1820
201
for state, entries in states.items():
1821
202
if 'error' in state:
1822
raise ErroredUnit(entries[0], state)
203
raise ErroredUnit(entries[0], state)
1825
def get_service_count(self):
1826
return len(self.status.get('services', {}))
1828
def get_service_unit_count(self, service):
1830
self.status.get('services', {}).get(service, {}).get('units', {}))
1832
206
def get_agent_versions(self):
1833
207
versions = defaultdict(set)
1834
208
for item_name, item in self.agent_items():
1835
if item.get('juju-status', None):
1836
version = item['juju-status'].get('version', 'unknown')
1837
versions[version].add(item_name)
1839
versions[item.get('agent-version', 'unknown')].add(item_name)
209
versions[item.get('agent-version', 'unknown')].add(item_name)
1842
def get_instance_id(self, machine_id):
1843
return self.status['machines'][machine_id]['instance-id']
1845
def get_unit(self, unit_name):
1846
"""Return metadata about a unit."""
1847
for service in sorted(self.status['services'].values()):
1848
if unit_name in service.get('units', {}):
1849
return service['units'][unit_name]
1850
raise KeyError(unit_name)
1852
def service_subordinate_units(self, service_name):
1853
"""Return subordinate metadata for a service_name."""
1854
services = self.status.get('services', {})
1855
if service_name in services:
1856
for unit in sorted(services[service_name].get(
1857
'units', {}).values()):
1858
for sub_name, sub in unit.get('subordinates', {}).items():
1861
def get_open_ports(self, unit_name):
1862
"""List the open ports for the specified unit.
1864
If no ports are listed for the unit, the empty list is returned.
1866
return self.get_unit(unit_name).get('open-ports', [])
1869
class SimpleEnvironment:
1871
def __init__(self, environment, config=None, juju_home=None,
1873
if controller is None:
1874
controller = Controller(environment)
1875
self.controller = controller
215
def __init__(self, environment, client=None, config=None):
1876
216
self.environment = environment
1877
218
self.config = config
1878
self.juju_home = juju_home
1879
219
if self.config is not None:
1880
220
self.local = bool(self.config.get('type') == 'local')
1882
222
self.local and bool(self.config.get('container') == 'kvm'))
1883
self.maas = bool(self.config.get('type') == 'maas')
1884
self.joyent = bool(self.config.get('type') == 'joyent')
224
'hpcloudsvc' in self.config.get('auth-url', ''))
1886
226
self.local = False
1891
def clone(self, model_name=None):
1892
config = deepcopy(self.config)
1893
if model_name is None:
1894
model_name = self.environment
1896
config['name'] = model_name
1897
result = self.__class__(model_name, config, self.juju_home,
1899
result.local = self.local
1900
result.kvm = self.kvm
1901
result.maas = self.maas
1902
result.joyent = self.joyent
1905
def __eq__(self, other):
1906
if type(self) != type(other):
1908
if self.environment != other.environment:
1910
if self.config != other.config:
1912
if self.local != other.local:
1914
if self.maas != other.maas:
1918
def __ne__(self, other):
1919
return not self == other
1921
def set_model_name(self, model_name, set_controller=True):
1923
self.controller.name = model_name
1924
self.environment = model_name
1925
self.config['name'] = model_name
1928
230
def from_config(cls, name):
1929
return cls._from_config(name)
1932
def _from_config(cls, name):
1933
config, selected = get_selected_environment(name)
1936
return cls(name, config)
231
client = JujuClientDevel.by_version()
232
return cls(name, client, get_selected_environment(name)[0])
1938
234
def needs_sudo(self):
1939
235
return self.local
1942
def make_jes_home(self, juju_home, dir_name, new_config):
1943
home_path = jes_home_path(juju_home, dir_name)
1944
if os.path.exists(home_path):
1946
os.makedirs(home_path)
1947
self.dump_yaml(home_path, new_config)
1950
def dump_yaml(self, path, config):
1951
dump_environments_yaml(path, config)
1954
class JujuData(SimpleEnvironment):
1956
def __init__(self, environment, config=None, juju_home=None,
1958
if juju_home is None:
1959
juju_home = get_juju_home()
1960
super(JujuData, self).__init__(environment, config, juju_home,
1962
self.credentials = {}
1965
def clone(self, model_name=None):
1966
result = super(JujuData, self).clone(model_name)
1967
result.credentials = deepcopy(self.credentials)
1968
result.clouds = deepcopy(self.clouds)
1972
def from_env(cls, env):
1973
juju_data = cls(env.environment, env.config, env.juju_home)
1974
juju_data.load_yaml()
1977
def load_yaml(self):
1978
with open(os.path.join(self.juju_home, 'credentials.yaml')) as f:
1979
self.credentials = yaml.safe_load(f)
1980
with open(os.path.join(self.juju_home, 'clouds.yaml')) as f:
1981
self.clouds = yaml.safe_load(f)
1984
def from_config(cls, name):
1985
juju_data = cls._from_config(name)
1986
juju_data.load_yaml()
1989
def dump_yaml(self, path, config):
1990
"""Dump the configuration files to the specified path.
1992
config is unused, but is accepted for compatibility with
1993
SimpleEnvironment and make_jes_home().
1995
with open(os.path.join(path, 'credentials.yaml'), 'w') as f:
1996
yaml.safe_dump(self.credentials, f)
1997
with open(os.path.join(path, 'clouds.yaml'), 'w') as f:
1998
yaml.safe_dump(self.clouds, f)
2000
def find_endpoint_cloud(self, cloud_type, endpoint):
2001
for cloud, cloud_config in self.clouds['clouds'].items():
2002
if cloud_config['type'] != cloud_type:
2004
if cloud_config['endpoint'] == endpoint:
2006
raise LookupError('No such endpoint: {}'.format(endpoint))
2008
def get_cloud(self):
2009
provider = self.config['type']
2010
# Separate cloud recommended by: Juju Cloud / Credentials / BootStrap /
2011
# Model CLI specification
2012
if provider == 'ec2' and self.config['region'] == 'cn-north-1':
2014
if provider not in ('maas', 'openstack'):
2018
}.get(provider, provider)
2019
if provider == 'maas':
2020
endpoint = self.config['maas-server']
2021
elif provider == 'openstack':
2022
endpoint = self.config['auth-url']
2023
return self.find_endpoint_cloud(provider, endpoint)
2025
def get_region(self):
2026
provider = self.config['type']
2027
if provider == 'azure':
2028
if 'tenant-id' not in self.config:
2029
raise ValueError('Non-ARM Azure not supported.')
2030
return self.config['location']
2031
elif provider == 'joyent':
2032
matcher = re.compile('https://(.*).api.joyentcloud.com')
2033
return matcher.match(self.config['sdc-url']).group(1)
2034
elif provider == 'lxd':
2036
elif provider == 'manual':
2037
return self.config['bootstrap-host']
2038
elif provider in ('maas', 'manual'):
2041
return self.config['region']
2044
class GroupReporter:
2046
def __init__(self, stream, expected):
2047
self.stream = stream
2048
self.expected = expected
2049
self.last_group = None
2051
self.wrap_offset = 0
2052
self.wrap_width = 79
2054
def _write(self, string):
2055
self.stream.write(string)
2062
def update(self, group):
2063
if group == self.last_group:
2064
if (self.wrap_offset + self.ticks) % self.wrap_width == 0:
2066
self._write("." if self.ticks or not self.wrap_offset else " .")
2070
for value, entries in sorted(group.items()):
2071
if value == self.expected:
2073
value_listing.append('%s: %s' % (value, ', '.join(entries)))
2074
string = ' | '.join(value_listing)
2075
lead_length = len(string) + 1
2077
string = "\n" + string
2079
self.last_group = group
2081
self.wrap_offset = lead_length if lead_length < self.wrap_width else 0
238
return self.client.bootstrap(self)
240
def upgrade_juju(self):
241
args = ('--version', self.get_matching_agent_version(no_build=True))
243
args += ('--upload-tools',)
244
self.client.juju(self, 'upgrade-juju', args)
246
def destroy_environment(self):
247
return self.client.destroy_environment(self)
249
def deploy(self, charm):
251
return self.juju('deploy', *args)
253
def juju(self, command, *args):
254
return self.client.juju(self, command, args)
256
def get_status(self, timeout=60):
257
return self.client.get_status(self, timeout)
259
def wait_for_started(self, timeout=1200):
260
"""Wait until all unit/machine agents are 'started'."""
261
for ignored in until_timeout(timeout):
263
status = self.get_status()
264
except CannotConnectEnv:
265
print('Supressing "Unable to connect to environment"')
267
states = status.check_agents_started(self.environment)
270
print(format_listing(states, 'started'))
273
raise Exception('Timed out waiting for agents to start in %s.' %
277
def wait_for_version(self, version, timeout=300):
278
for ignored in until_timeout(timeout):
280
versions = self.get_status(120).get_agent_versions()
281
except CannotConnectEnv:
282
print('Supressing "Unable to connect to environment"')
284
if versions.keys() == [version]:
286
print(format_listing(versions, version))
289
raise Exception('Some versions did not update.')
291
def get_matching_agent_version(self, no_build=False):
292
# strip the series and srch from the built version.
293
version_parts = self.client.version.split('-')
294
if len(version_parts) == 4:
295
version_number = '-'.join(version_parts[0:2])
297
version_number = version_parts[0]
298
if not no_build and self.local:
299
version_number += '.1'
300
return version_number
302
def set_testing_tools_metadata_url(self):
303
url = self.client.get_env_option(self, 'tools-metadata-url')
304
if 'testing' not in url:
305
testing_url = url.replace('/tools', '/testing/tools')
306
self.client.set_env_option(self, 'tools-metadata-url', testing_url)
309
def format_listing(listing, expected):
311
for value, entries in listing.items():
312
if value == expected:
314
value_listing.append('%s: %s' % (value, ', '.join(entries)))
315
return ' | '.join(value_listing)