Permalink
bce1eca Dec 23, 2017
@battlemidget @johnsca @mikemccracken @petevg
964 lines (784 sloc) 29.3 KB
""" Juju helpers
"""
import asyncio
import json
import logging
import os
from concurrent import futures
from pathlib import Path
from subprocess import DEVNULL, PIPE, CalledProcessError
from tempfile import NamedTemporaryFile
import yaml
from bundleplacer.charmstore_api import CharmStoreID
from juju.constraints import parse as parse_constraints
from juju.model import Model
from conjureup import consts, errors, events, utils
from conjureup.app_config import app
from conjureup.utils import is_linux, juju_path, run, spew
JUJU_ASYNC_QUEUE = "juju-async-queue"
PENDING_DEPLOYS = 0
def _check_bin_candidates(candidates, bin_property):
""" Checks a list of binary paths to verify they exist and are
executable
"""
# search candidate paths, in order, for the binary (ie juju, juju-wait)
# we don't use $PATH because we have definite preferences which one we use
# and we don't want to leave it up to the user
if not hasattr(app.juju, bin_property):
raise errors.AppConfigAttributeError(
"Unknown juju property: {}".format(bin_property))
for candidate in candidates:
if os.access(candidate, os.X_OK):
setattr(app.juju, bin_property, candidate)
app.log.debug("{} candidate found".format(bin_property))
break
else:
raise errors.JujuBinaryNotFound(
"Unable to locate a candidate executable for {}.".format(
candidates))
def set_bin_path():
""" Sets the juju binary path
"""
candidates = [
'/snap/bin/juju',
'/snap/bin/conjure-up.juju',
'/usr/bin/juju',
'/usr/local/bin/juju',
]
_check_bin_candidates(candidates, 'bin_path')
# Update $PATH so that we make sure this candidate is used
# first.
app.env['PATH'] = "{}:{}".format(Path(app.juju.bin_path).parent,
app.env['PATH'])
def set_wait_path():
""" Sets juju-wait path
"""
candidates = [
'/snap/bin/juju-wait',
'/snap/bin/conjure-up.juju-wait',
'/usr/bin/juju-wait',
'/usr/local/bin/juju-wait',
]
_check_bin_candidates(candidates, 'wait_path')
def read_config(name):
""" Reads a juju config file
Arguments:
name: filename without extension (ext defaults to yaml)
Returns:
dictionary of yaml object
"""
abs_path = os.path.join(juju_path(), "{}.yaml".format(name))
if not os.path.isfile(abs_path):
raise Exception("Cannot load {}".format(abs_path))
return yaml.safe_load(open(abs_path))
def get_bootstrap_config(controller_name):
try:
bootstrap_config = read_config("bootstrap-config")
except Exception:
# We may be trying to access the bootstrap-config to quickly
# between the time of juju bootstrap occurs and this function
# is accessed.
app.log.exception("Could not load bootstrap-config, "
"setting an empty controllers dict.")
bootstrap_config = dict(controllers={})
if 'controllers' not in bootstrap_config:
raise Exception("Could not read Juju's bootstrap-config.yaml")
cd = bootstrap_config['controllers'].get(controller_name, None)
if cd is None:
raise errors.ControllerNotFoundException(
"'{}' not found in Juju's "
"bootstrap-config.yaml".format(controller_name))
return cd
def get_current_controller():
""" Grabs the current default controller
"""
try:
return get_controllers()['current-controller']
except KeyError:
return None
def get_controller(id):
""" Return specific controller
Arguments:
id: controller id
"""
if 'controllers' in get_controllers() \
and id in get_controllers()['controllers']:
return get_controllers()['controllers'][id]
return None
def get_controller_in_cloud(cloud):
""" Returns a controller that is bootstrapped on the named cloud
Arguments:
cloud: cloud to check for
Returns:
available controller or None if nothing available
"""
controllers = get_controllers()['controllers'].items()
for controller_name, controller in controllers:
if cloud == controller['cloud']:
return controller_name
return None
async def login():
""" Login to Juju API server
"""
if app.juju.authenticated:
return
if app.provider.controller is None:
raise Exception("Unable to determine current controller")
if app.provider.model is None:
raise Exception("Tried to login with no current model set.")
app.juju.client = Model(app.loop)
model_name = '{}:{}'.format(app.provider.controller,
app.provider.model)
if not events.ModelAvailable.is_set():
await events.ModelAvailable.wait()
app.log.info('Connecting to model {}...'.format(model_name))
await app.juju.client.connect_model(model_name)
app.juju.authenticated = True
events.ModelConnected.set()
app.log.info('Connected')
async def bootstrap(controller, cloud, model='conjure-up', series="xenial",
credential=None):
""" Performs juju bootstrap
If not LXD pass along the newly defined credentials
Arguments:
controller: name of your controller
cloud: name of local or public cloud to deploy to
model: name of default model to create
series: define the bootstrap series defaults to xenial
credential: credentials key
"""
if app.provider.region is not None:
app.log.debug("Bootstrapping to set region: {}")
cloud = "{}/{}".format(app.provider.cloud, app.provider.region)
cmd = [app.juju.bin_path, "bootstrap",
cloud, controller, "--default-model", model]
def add_config(k, v):
cmd.extend(["--config", "{}={}".format(k, v)])
if app.provider.model_defaults:
for k, v in app.provider.model_defaults.items():
if v is not None:
add_config(k, v)
add_config("image-stream", "daily")
if app.argv.http_proxy:
add_config("http-proxy", app.argv.http_proxy)
if app.argv.https_proxy:
add_config("https-proxy", app.argv.https_proxy)
if app.argv.apt_http_proxy:
add_config("apt-http-proxy", app.argv.apt_http_proxy)
if app.argv.apt_https_proxy:
add_config("apt-https-proxy", app.argv.apt_https_proxy)
if app.argv.no_proxy:
add_config("no-proxy", app.argv.no_proxy)
if app.argv.bootstrap_timeout:
add_config("bootstrap-timeout", app.argv.bootstrap_timeout)
if app.argv.bootstrap_to:
cmd.extend(["--to", app.argv.bootstrap_to])
cmd.extend(["--bootstrap-series", series])
if credential is not None:
cmd.extend(["--credential", credential])
if app.argv.debug:
cmd.append("--debug")
app.log.debug("bootstrap cmd: {}".format(cmd))
log_file = '{}-bootstrap'.format(app.provider.controller)
path_base = str(Path(app.config['spell-dir']) / log_file)
out_path = path_base + '.out'
err_path = path_base + '.err'
rc, _, _ = await utils.arun(cmd, stdout=out_path, stderr=err_path)
if rc < 0:
raise errors.BootstrapInterrupt('Bootstrap killed by user')
elif rc > 0:
return False
events.ModelAvailable.set()
return True
def has_jaas_auth():
jaas_cookies = Path('~/.local/share/juju/cookies/jaas.json').expanduser()
if jaas_cookies.exists():
jaas_cookies = json.loads(jaas_cookies.read_text())
for cookie in jaas_cookies or []:
if cookie['Domain'] == consts.JAAS_DOMAIN:
return bool(cookie['Value'])
return False
async def register_controller(name, endpoint, email, password, twofa,
timeout=30, fail_cb=None, timeout_cb=None):
app.log.info('Registering controller {}'.format(name))
cmd = ['juju', 'login', '-B', endpoint, '-c', name]
proc = await asyncio.create_subprocess_exec(
*cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE,
)
try:
stdin = b''.join(b'%s\n' % bytes(f, 'utf8')
for f in [email, password, twofa])
stdout, stderr = await asyncio.wait_for(proc.communicate(stdin),
timeout)
stdout = stdout.decode('utf8')
stderr = stderr.decode('utf8')
prefix = 'Enter a name for this controller: '
if stderr.startswith(prefix):
# Juju has started putting this one prompt out on stderr
# instead of stdout for some reason, so we work around it.
stderr = stderr[len(prefix):]
except asyncio.TimeoutError:
proc.kill()
app.log.warning('Registration timed out')
if timeout_cb:
timeout_cb()
elif fail_cb:
fail_cb('Timed out')
return False
if proc.returncode != 0:
app.log.warning('Registration failed: {}'.format(stderr))
if fail_cb:
fail_cb(stderr)
return False
else:
raise CalledProcessError(cmd, stderr)
app.log.info('Registration complete')
return True
async def model_available(name):
""" Checks if juju is available
Returns:
True/False if juju status was successful and a working model is found
"""
proc = await asyncio.create_subprocess_exec(
'juju', 'status', '-m', ':'.join([app.provider.controller, name]),
stderr=DEVNULL,
stdout=DEVNULL)
await proc.wait()
return proc.returncode == 0
def autoload_credentials():
""" Automatically checks known places for cloud credentials
"""
try:
run('{} autoload-credentials'.format(
app.juju.bin_path), shell=True, check=True)
except CalledProcessError:
return False
return True
def get_credential(cloud, cred_name=None):
""" Get credential
Arguments:
cloud: cloud applicable to user credentials
cred_name: name of credential to get, or default
"""
creds = get_credentials()
if cloud not in creds.keys():
return None
cred = creds[cloud]
default_credential = cred.pop('default-credential', None)
cred.pop('default-region', None)
if cred_name is not None and cred_name in cred.keys():
return cred[cred_name]
elif default_credential is not None and default_credential in cred.keys():
return cred[default_credential]
elif len(cred) == 1:
return list(cred.values())[0]
else:
return None
def get_credentials(secrets=True):
""" List credentials
This will fallback to reading the credentials file directly
Arguments:
secrets: True/False whether to show secrets (ie password)
Returns:
List of credentials
"""
cmd = '{} list-credentials --format yaml'.format(app.juju.bin_path)
if secrets:
cmd += ' --show-secrets'
sh = run(cmd, shell=True, stdout=PIPE, stderr=PIPE)
if sh.returncode > 0:
try:
env = read_config('credentials')
return env['credentials']
except:
raise Exception(
"Unable to list credentials: {}".format(
sh.stderr.decode('utf8')))
env = yaml.safe_load(sh.stdout.decode('utf8'))
return env['credentials']
def get_regions(cloud):
""" List available regions for cloud
Arguments:
cloud: Cloud to list regions for
Returns:
Dictionary of all known regions for cloud
"""
sh = run('{} list-regions {} --format yaml'.format(app.juju.bin_path,
cloud),
shell=True, stdout=PIPE, stderr=PIPE)
stdout = sh.stdout.decode('utf8')
stderr = sh.stderr.decode('utf8')
if sh.returncode > 0:
raise Exception("Unable to list regions: {}".format(stderr))
if 'no regions' in stdout:
return {}
result = yaml.safe_load(stdout)
if not isinstance(result, dict):
msg = 'Unexpected response from regions: {}'.format(result)
app.log.error(msg)
utils.sentry_report(msg, level=logging.ERROR)
result = {}
return result
def get_clouds():
""" List available clouds
Returns:
Dictionary of all known clouds including newly created MAAS/Local
"""
sh = run('{} list-clouds --format yaml'.format(app.juju.bin_path),
shell=True, stdout=PIPE, stderr=PIPE)
if sh.returncode > 0:
raise Exception(
"Unable to list clouds: {}".format(sh.stderr.decode('utf8'))
)
return yaml.safe_load(sh.stdout.decode('utf8'))
def get_compatible_clouds(cloud_types=None):
""" List cloud types compatible with the current spell and controller.
Arguments:
clouds: optional initial list of clouds to filter
Returns:
List of cloud types
"""
if cloud_types is None:
clouds = get_clouds()
cloud_types = set(c['type'] for c in clouds.values())
# custom providers don't show up in list-clouds but are valid types
cloud_types |= set(consts.CUSTOM_PROVIDERS)
else:
cloud_types = set(cloud_types)
_normalize_cloud_types(cloud_types)
if not is_linux():
# LXD not available on macOS
cloud_types -= {'localhost'}
if app.provider and app.provider.controller:
# if we already have a controller, we should query
# it via the API for what clouds it supports; for now,
# though, just assume it's JAAS and hard-code the options
cloud_types &= consts.JAAS_CLOUDS
whitelist = set(app.config['metadata'].get('cloud-whitelist', []))
blacklist = set(app.config['metadata'].get('cloud-blacklist', []))
addons_dir = Path(app.config['spell-dir']) / 'addons'
for addon in app.selected_addons:
addon_file = addons_dir / addon / 'metadata.yaml'
addon_meta = yaml.safe_load(addon_file.read_text())
whitelist.update(addon_meta.get('cloud-whitelist', []))
blacklist.update(addon_meta.get('cloud-blacklist', []))
_normalize_cloud_types(whitelist)
_normalize_cloud_types(blacklist)
if len(whitelist) > 0:
return sorted(cloud_types & whitelist)
elif len(blacklist) > 0:
return sorted(cloud_types ^ blacklist)
return sorted(cloud_types)
def _normalize_cloud_types(cloud_types):
if 'lxd' in cloud_types:
# normalize 'lxd' cloud type to localhost; 'lxd' can happen
# depending on how the controller was bootstrapped
cloud_types -= {'lxd'}
cloud_types |= {'localhost'}
if 'local' in cloud_types:
cloud_types -= {'local'}
cloud_types |= {'localhost'}
if 'aws' in cloud_types:
cloud_types -= {'aws'}
cloud_types |= {'ec2'}
if 'google' in cloud_types:
cloud_types -= {'google'}
cloud_types |= {'gce'}
def get_cloud_types_by_name():
""" Return a mapping of cloud names to their type.
This accounts for some normalizations that get_clouds() doesn't.
"""
clouds = {n: c['type'] for n, c in get_clouds().items()}
# normalize 'lxd' cloud type to localhost; 'lxd' can happen
# depending on how the controller was bootstrapped
for name, cloud_type in clouds.items():
if cloud_type == 'lxd':
clouds[name] = 'localhost'
for provider in consts.CUSTOM_PROVIDERS:
if provider not in clouds:
clouds[provider] = provider
return clouds
def add_cloud(name, config):
""" Adds a cloud
Arguments:
name: name of cloud to add
config: cloud configuration
"""
_config = {
'clouds': {
name: config
}
}
app.log.debug(_config)
with NamedTemporaryFile(mode='w', encoding='utf-8',
delete=False) as tempf:
output = yaml.safe_dump(_config, default_flow_style=False)
spew(tempf.name, output)
sh = run('{} add-cloud {} {}'.format(app.juju.bin_path,
name, tempf.name),
shell=True, stdout=PIPE, stderr=PIPE)
if sh.returncode > 0:
raise Exception(
"Unable to add cloud: {}".format(sh.stderr.decode('utf8')))
def get_cloud(name):
""" Return specific cloud information
Arguments:
name: name of cloud to query, ie. aws, lxd, local:provider
Returns:
Dictionary of cloud attributes
"""
if name in get_clouds().keys():
return get_clouds()[name]
raise LookupError("Unable to locate cloud: {}".format(name))
def constraints_to_dict(constraints):
"""
Parses a constraint string into a dict. If tags and spaces are found they
will be converted into a list. All other constraints are passed directly to
juju for processing during deployment.
"""
new_constraints = {}
if not isinstance(constraints, str):
app.log.debug(
"Invalid constraints: {}, skipping".format(
constraints))
return new_constraints
list_constraints = [c for c in constraints.split(' ')
if c != ""]
for c in list_constraints:
try:
constraint, value = c.split('=')
if constraint in ['tags', 'spaces']:
value = value.split(',')
else:
pass
new_constraints[constraint] = value
except ValueError as e:
app.log.debug("Skipping constraint: {} ({})".format(c, e))
return new_constraints
def constraints_from_dict(cdict):
return " ".join(["{}={}".format(k, v) for k, v in cdict.items()])
def deploy(bundle):
""" Juju deploy bundle
Arguments:
bundle: Name of bundle to deploy, can be a path to local bundle file or
charmstore path.
"""
try:
return run('{} deploy {}'.format(app.juju.bin_path,
bundle), shell=True,
stdout=DEVNULL, stderr=PIPE)
except CalledProcessError as e:
raise e
async def add_machines(applications, machines, msg_cb):
"""Add machines to model
Arguments:
app: name of app to which the machines belong
machines: a mapping of virtual machine numbers to machine attributes.
The key 'series' is required, and 'constraints' is the only other
supported key
"""
if not events.PreDeployComplete.is_set():
# block until after pre-deploy
await events.PreDeployComplete.wait()
new_machines = {}
tasks = []
for vmid in sorted(machines.keys()):
if events.MachineCreated.is_set(vmid):
tasks.append(asyncio.sleep(0)) # no-op
elif events.MachinePending.is_set(vmid):
tasks.append(events.MachineCreated.wait(vmid))
else:
events.MachinePending.set(vmid)
machine = machines[vmid]
series = machine['series']
constraints = parse_constraints(machine.get('constraints', ''))
tasks.append(app.juju.client.add_machine(series=series,
constraints=constraints))
new_machines[vmid] = None
if new_machines:
msg = 'Adding machine{}: {}'.format(
's' if len(new_machines) > 1 else '',
', '.join(
'{}: ({}, {})'.format(v,
machines[v]['series'],
machines[v].get('constraints', ''))
for v in sorted(new_machines.keys())),
)
app.log.info(msg)
msg_cb(msg)
else:
app.log.info('No new machines to add for {}'.format(
', '.join(a.service_name for a in applications)))
results = await asyncio.gather(*tasks)
for vmid, task_result in zip(sorted(machines.keys()), results):
if vmid not in new_machines:
# this is an events.MachinePending.wait() or no-op result
continue
events.MachinePending.clear(vmid)
events.MachineCreated.set(vmid)
new_machines[vmid] = task_result.id
if new_machines:
msg = "Added machine{}: {}".format(
's' if len(new_machines) > 1 else '',
', '.join(sorted(new_machines.keys())),
)
app.log.info(msg)
msg_cb(msg)
for application in applications:
app.log.info('Machines available for application {}'.format(
application.service_name
))
events.AppMachinesCreated.set(application.service_name)
return new_machines
async def deploy_service(service, default_series, msg_cb):
"""Juju deploy service.
If the service's charm ID doesn't have a revno, will query charm
store to get latest revno for the charm.
If the service's charm ID has a series, use that, otherwise use
the provided default series.
Arguments:
service: Service to deploy
msg_cb: message callback
exc_cb: exception handler callback
Returns a future that will be completed after the deploy has been
submitted to juju
"""
name = service.service_name
if not events.AppMachinesCreated.is_set(name):
# block until we have machines
await events.AppMachinesCreated.wait(name)
app.log.debug('Machines for {} are ready'.format(name))
if service.csid.rev == "":
id_no_rev = service.csid.as_str_without_rev()
mc = app.metadata_controller
futures.wait([mc.metadata_future])
info = mc.get_charm_info(id_no_rev, lambda _: None)
service.csid = CharmStoreID(info["Id"])
deploy_args = {}
deploy_args = dict(
entity_url=service.csid.as_str(),
application_name=service.service_name,
num_units=service.num_units,
constraints=service.constraints,
to=service.placement_spec,
config=service.options,
)
msg = 'Deploying {}...'.format(service.service_name)
app.log.info(msg)
msg_cb(msg)
from pprint import pformat
app.log.debug(pformat(deploy_args))
app_inst = await app.juju.client.deploy(**deploy_args)
if service.expose:
msg = 'Exposing {}.'.format(service.service_name)
app.log.info(msg)
msg_cb(msg)
await app_inst.expose()
msg = '{}: deployed, installing.'.format(service.service_name)
app.log.info(msg)
msg_cb(msg)
events.AppDeployed.set(service.service_name)
async def set_relations(service, msg_cb):
""" Juju set relations
Arguments:
service: service with relations to set
"""
relations = set()
for a, b in service.relations:
rel_pair = tuple(sorted((a, b)))
if rel_pair in relations:
continue
a_app = a.split(':')[0]
b_app = b.split(':')[0]
await asyncio.gather(
events.AppDeployed.wait(a_app),
events.AppDeployed.wait(b_app),
)
relations.add(rel_pair)
app.log.debug('Adding relations for %s: %s',
service.service_name, relations)
try:
for rel_pair in relations:
rel_name = '{} <-> {}'.format(*rel_pair)
pending = events.PendingRelations.is_set(rel_name)
added = events.RelationsAdded.is_set(rel_name)
if pending or added:
continue
msg = "Setting relation {}".format(rel_name)
app.log.info(msg)
msg_cb(msg)
events.PendingRelations.set(rel_name)
await app.juju.client.add_relation(*rel_pair)
events.PendingRelations.clear(rel_name)
events.RelationsAdded.set(rel_name)
events.RelationsAdded.set(service.service_name)
except Exception:
app.log.exception('Error adding relations for %s',
service.service_name)
raise
def get_controller_info(name=None):
""" Returns information on current controller
Arguments:
name: if set shows info controller, otherwise displays current.
"""
cmd = '{} show-controller --format yaml'.format(
app.juju.bin_path)
if name is not None:
cmd += ' {}'.format(name)
sh = run(cmd, shell=True, stdout=PIPE, stderr=PIPE)
sh_out = sh.stdout.decode('utf8')
sh_err = sh.stderr.decode('utf8')
try:
data = yaml.safe_load(sh_out)
except yaml.parser.ParserError:
data = None
if sh.returncode != 0 or not data:
raise Exception("Unable to get info for "
"controller {}: {}".format(name, sh_err))
return next(iter(data.values()))
def get_controllers():
""" List available controllers
Returns:
List of known controllers
"""
sh = run('{} list-controllers --format yaml'.format(
app.juju.bin_path),
shell=True, stdout=PIPE, stderr=PIPE)
if sh.returncode > 0:
raise LookupError(
"Unable to list controllers: {}".format(sh.stderr.decode('utf8')))
env = yaml.safe_load(sh.stdout.decode('utf8'))
return env
def get_account(controller):
""" List account information for controller
Arguments:
controller: controller id
Returns:
Dictionary containing list of accounts for controller and the
current account in use.
"""
return get_accounts().get(controller, {})
def get_accounts():
""" List available accounts
Returns:
List of known accounts
"""
env = os.path.join(juju_path(), 'accounts.yaml')
if not os.path.isfile(env):
raise Exception(
"Unable to find: {}".format(env))
with open(env, 'r') as c:
env = yaml.load(c)
return env['controllers']
raise Exception("Unable to find accounts")
def get_model(controller, name):
""" List information for model
Arguments:
name: model name
controller: name of controller to work in
Returns:
Dictionary of model information
"""
models = get_models(controller)['models']
for m in models:
if m['short-name'] == name:
return m
raise LookupError(
"Unable to find model: {}".format(name))
async def add_model(name, controller, cloud, credential=None):
""" Adds a model to current controller
Arguments:
controller: controller to add model in
cloud: cloud/region to add model in
credential: optional credential name to use (required unless localhost)
"""
if await model_available(name):
events.ModelAvailable.set()
await login()
return
cmd = ['juju', 'add-model', name, cloud, '--controller', controller]
if credential:
cmd.extend(['--credential', credential])
def add_model_config(k, v):
cmd.extend(["--config", "{}={}".format(k, v)])
if app.provider.model_defaults:
for k, v in app.provider.model_defaults.items():
if v is not None:
add_model_config(k, v)
proc = await asyncio.create_subprocess_exec(*cmd,
stdout=DEVNULL, stderr=PIPE)
_, stderr = await proc.communicate()
if proc.returncode > 0:
raise Exception(
"Unable to create model: {}".format(stderr.decode('utf8')))
# the CLI has to connect to the model at least once to
# populate the model macaroons; model_available does this
# and verifies the model is working
if not await model_available(name):
raise Exception("Unable to connect model after creation")
events.ModelAvailable.set()
await login()
async def destroy_model(controller, model):
""" Destroys a model within a controller
Arguments:
controller: name of controller
model: name of model to destroy
"""
proc = await asyncio.create_subprocess_exec(
'juju', 'destroy-model', '-y', ':'.join([controller, model]),
stdout=DEVNULL, stderr=PIPE)
_, stderr = await proc.communicate()
if proc.returncode > 0:
raise Exception(
"Unable to destroy model: {}".format(stderr.decode('utf8')))
events.ModelAvailable.clear()
def get_models(controller):
""" List available models
Arguments:
controller: existing controller to get models for
Returns:
List of known models
"""
sh = run('{} list-models --format yaml -c {}'.format(app.juju.bin_path,
controller),
shell=True, stdout=PIPE, stderr=PIPE)
if sh.returncode > 0:
raise LookupError(
"Unable to list models: {}".format(sh.stderr.decode('utf8')))
out = yaml.safe_load(sh.stdout.decode('utf8'))
return out
def get_current_model():
try:
return get_models()['current-model']
except:
return None
def version():
""" Returns version of Juju
"""
sh = run('{} version'.format(
app.juju.bin_path),
shell=True, stdout=PIPE, stderr=PIPE)
if sh.returncode > 0:
raise Exception(
"Unable to get Juju Version".format(sh.stderr.decode('utf8')))
out = sh.stdout.decode('utf8')
if isinstance(out, list):
return out.pop()
else:
return out
async def wait_for_deployment(retries=3):
""" Waits for all deployed applications to settle
"""
if 'CONJURE_UP_MODE' in app.env and app.env['CONJURE_UP_MODE'] == "test":
retries = 0
cmd = [app.juju.wait_path, "-r{}".format(retries),
"-vwm", "{}:{}".format(app.provider.controller,
app.provider.model)]
out_path = str(Path(app.config['spell-dir']) / 'deploy-wait.out')
err_path = str(Path(app.config['spell-dir']) / 'deploy-wait.err')
ret, _, err_log = await utils.arun(cmd, stdout=out_path, stderr=err_path)
if ret != 0:
err_log_tail = err_log.splitlines()[-10:]
app.log.error('\n'.join(err_log_tail))
raise errors.DeploymentFailure(
"Some applications failed to start successfully.")