1033 lines
35 KiB
Python
1033 lines
35 KiB
Python
"""Module for interacting with a juju model.
|
|
|
|
This module contains a number of functions for interacting with a Juju model
|
|
mostly via libjuju. Async function also provice a non-async alternative via
|
|
'sync_wrapper'.
|
|
"""
|
|
|
|
import asyncio
|
|
from async_generator import async_generator, yield_, asynccontextmanager
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import yaml
|
|
from oslo_config import cfg
|
|
|
|
from juju.errors import JujuError
|
|
from juju.model import Model
|
|
|
|
from zaza import sync_wrapper
|
|
|
|
CURRENT_MODEL = None
|
|
|
|
|
|
def set_juju_model(model_name):
|
|
"""Point environment at the given model.
|
|
|
|
:param model_name: Model to point environment at
|
|
:type model_name: str
|
|
"""
|
|
global CURRENT_MODEL
|
|
os.environ["JUJU_MODEL"] = model_name
|
|
CURRENT_MODEL = model_name
|
|
|
|
|
|
def get_juju_model():
|
|
"""Retrieve current model.
|
|
|
|
First check the environment for JUJU_MODEL. If this is not set, get the
|
|
current active model.
|
|
|
|
:returns: In focus model name
|
|
:rtype: str
|
|
"""
|
|
global CURRENT_MODEL
|
|
if CURRENT_MODEL:
|
|
return CURRENT_MODEL
|
|
# LY: I think we should remove the KeyError handling. I don't think we
|
|
# should ever fall back to the model in focus because it will lead
|
|
# to functions being added which do not explicitly set a model and
|
|
# zaza will loose the ability to do concurrent runs.
|
|
try:
|
|
# Check the environment
|
|
CURRENT_MODEL = os.environ["JUJU_MODEL"]
|
|
except KeyError:
|
|
try:
|
|
CURRENT_MODEL = os.environ["MODEL_NAME"]
|
|
except KeyError:
|
|
# If unset connect get the current active model
|
|
CURRENT_MODEL = get_current_model()
|
|
return CURRENT_MODEL
|
|
|
|
|
|
async def deployed():
|
|
"""List deployed applications."""
|
|
# Create a Model instance. We need to connect our Model to a Juju api
|
|
# server before we can use it.
|
|
model = Model()
|
|
# Connect to the currently active Juju model
|
|
await model.connect_current()
|
|
try:
|
|
# list currently deploeyd services
|
|
return list(model.applications.keys())
|
|
finally:
|
|
# Disconnect from the api server and cleanup.
|
|
await model.disconnect()
|
|
|
|
|
|
def get_unit_from_name(unit_name, model):
|
|
"""Return the units that corresponds to the name in the given model.
|
|
|
|
:param unit_name: Name of unit to match
|
|
:type unit_name: str
|
|
:param model: Model to perform lookup in
|
|
:type model: juju.model.Model
|
|
:returns: Unit matching given name
|
|
:rtype: juju.unit.Unit or None
|
|
"""
|
|
app = unit_name.split('/')[0]
|
|
unit = None
|
|
for u in model.applications[app].units:
|
|
if u.entity_id == unit_name:
|
|
unit = u
|
|
break
|
|
else:
|
|
raise Exception
|
|
return unit
|
|
|
|
|
|
@asynccontextmanager
|
|
@async_generator
|
|
async def run_in_model(model_name):
|
|
"""Context manager for executing code inside a libjuju model.
|
|
|
|
Example of using run_in_model:
|
|
async with run_in_model(model_name) as model:
|
|
model.do_something()
|
|
|
|
:param model_name: Name of model to run function in
|
|
:type model_name: str
|
|
:returns: The juju Model object correcsponding to model_name
|
|
:rtype: Iterator[:class:'juju.Model()']
|
|
"""
|
|
model = Model()
|
|
if not model_name:
|
|
model_name = get_juju_model()
|
|
await model.connect_model(model_name)
|
|
await yield_(model)
|
|
await model.disconnect()
|
|
|
|
|
|
async def async_scp_to_unit(unit_name, source, destination, model_name=None,
|
|
user='ubuntu', proxy=False, scp_opts=''):
|
|
"""Transfer files to unit_name in model_name.
|
|
|
|
:param model_name: Name of model unit is in
|
|
:type model_name: str
|
|
:param unit_name: Name of unit to scp to
|
|
:type unit_name: str
|
|
:param source: Local path of file(s) to transfer
|
|
:type source: str
|
|
:param destination: Remote destination of transferred files
|
|
:type source: str
|
|
:param user: Remote username
|
|
:type source: str
|
|
:param proxy: Proxy through the Juju API server
|
|
:type proxy: bool
|
|
:param scp_opts: Additional options to the scp command
|
|
:type scp_opts: str
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
unit = get_unit_from_name(unit_name, model)
|
|
await unit.scp_to(source, destination, user=user, proxy=proxy,
|
|
scp_opts=scp_opts)
|
|
|
|
scp_to_unit = sync_wrapper(async_scp_to_unit)
|
|
|
|
|
|
async def async_scp_to_all_units(application_name, source, destination,
|
|
model_name=None, user='ubuntu', proxy=False,
|
|
scp_opts=''):
|
|
"""Transfer files from to all units of an application.
|
|
|
|
:param model_name: Name of model unit is in
|
|
:type model_name: str
|
|
:param application_name: Name of application to scp file to
|
|
:type application_name: str
|
|
:param source: Local path of file(s) to transfer
|
|
:type source: str
|
|
:param destination: Remote destination of transferred files
|
|
:type source: str
|
|
:param user: Remote username
|
|
:type source: str
|
|
:param proxy: Proxy through the Juju API server
|
|
:type proxy: bool
|
|
:param scp_opts: Additional options to the scp command
|
|
:type scp_opts: str
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
for unit in model.applications[application_name].units:
|
|
# FIXME: Should scp in parallel
|
|
await unit.scp_to(source, destination, user=user, proxy=proxy,
|
|
scp_opts=scp_opts)
|
|
|
|
scp_to_all_units = sync_wrapper(async_scp_to_all_units)
|
|
|
|
|
|
async def async_scp_from_unit(unit_name, source, destination, model_name=None,
|
|
user='ubuntu', proxy=False, scp_opts=''):
|
|
"""Transfer files from to unit_name in model_name.
|
|
|
|
:param model_name: Name of model unit is in
|
|
:type model_name: str
|
|
:param unit_name: Name of unit to scp from
|
|
:type unit_name: str
|
|
:param source: Remote path of file(s) to transfer
|
|
:type source: str
|
|
:param destination: Local destination of transferred files
|
|
:type source: str
|
|
:param user: Remote username
|
|
:type source: str
|
|
:param proxy: Proxy through the Juju API server
|
|
:type proxy: bool
|
|
:param scp_opts: Additional options to the scp command
|
|
:type scp_opts: str
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
unit = get_unit_from_name(unit_name, model)
|
|
await unit.scp_from(source, destination, user=user, proxy=proxy,
|
|
scp_opts=scp_opts)
|
|
|
|
|
|
scp_from_unit = sync_wrapper(async_scp_from_unit)
|
|
|
|
|
|
async def async_run_on_unit(unit_name, command, model_name=None, timeout=None):
|
|
"""Juju run on unit.
|
|
|
|
:param model_name: Name of model unit is in
|
|
:type model_name: str
|
|
:param unit_name: Name of unit to match
|
|
:type unit: str
|
|
:param command: Command to execute
|
|
:type command: str
|
|
:param timeout: How long in seconds to wait for command to complete
|
|
:type timeout: int
|
|
:returns: action.data['results'] {'Code': '', 'Stderr': '', 'Stdout': ''}
|
|
:rtype: dict
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
unit = get_unit_from_name(unit_name, model)
|
|
action = await unit.run(command, timeout=timeout)
|
|
if action.data.get('results'):
|
|
return action.data.get('results')
|
|
else:
|
|
return {}
|
|
|
|
run_on_unit = sync_wrapper(async_run_on_unit)
|
|
|
|
|
|
async def async_get_unit_time(unit_name, model_name=None, timeout=None):
|
|
"""Get the current time (in seconds since Epoch) on the given unit.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param unit_name: Name of unit to run action on
|
|
:type unit_name: str
|
|
:returns: time in seconds since Epoch on unit
|
|
:rtype: int
|
|
"""
|
|
out = await async_run_on_unit(
|
|
model_name,
|
|
unit_name,
|
|
"date +'%s'",
|
|
timeout=timeout)
|
|
return int(out['Stdout'])
|
|
|
|
get_unit_time = sync_wrapper(async_get_unit_time)
|
|
|
|
|
|
async def async_get_unit_service_start_time(unit_name, service,
|
|
model_name=None, timeout=None):
|
|
"""Return the time that the given service was started on a unit.
|
|
|
|
Return the time (in seconds since Epoch) that the given service was
|
|
started on the given unit. If the service is not running raise
|
|
ServiceNotRunning exception.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param unit_name: Name of unit to run action on
|
|
:type unit_name: str
|
|
:param service: Name of service to check is running
|
|
:type service: str
|
|
:param timeout: Time to wait for status to be achieved
|
|
:type timeout: int
|
|
:returns: time in seconds since Epoch on unit
|
|
:rtype: int
|
|
:raises: ServiceNotRunning
|
|
"""
|
|
cmd = "stat -c %Y /proc/$(pidof -x {} | cut -f1 -d ' ')".format(service)
|
|
out = await async_run_on_unit(model_name, unit_name, cmd, timeout=timeout)
|
|
out = out['Stdout'].strip()
|
|
if out:
|
|
return int(out)
|
|
else:
|
|
raise ServiceNotRunning(service)
|
|
|
|
get_unit_service_start_time = sync_wrapper(async_get_unit_service_start_time)
|
|
|
|
|
|
async def async_get_application(application_name, model_name=None):
|
|
"""Return an application object.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application to retrieve units for
|
|
:type application_name: str
|
|
:returns: Application object
|
|
:rtype: object
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
return model.applications[application_name]
|
|
|
|
get_application = sync_wrapper(async_get_application)
|
|
|
|
|
|
async def async_get_units(application_name, model_name=None):
|
|
"""Return all the units of a given application.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application to retrieve units for
|
|
:type application_name: str
|
|
:returns: List of juju units
|
|
:rtype: [juju.unit.Unit, juju.unit.Unit,...]
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
return model.applications[application_name].units
|
|
|
|
get_units = sync_wrapper(async_get_units)
|
|
|
|
|
|
async def async_get_machines(application_name, model_name=None):
|
|
"""Return all the machines of a given application.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application to retrieve units for
|
|
:type application_name: str
|
|
:returns: List of juju machines
|
|
:rtype: [juju.machine.Machine, juju.machine.Machine,...]
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
machines = []
|
|
for unit in model.applications[application_name].units:
|
|
machines.append(unit.machine)
|
|
return machines
|
|
|
|
get_machines = sync_wrapper(async_get_machines)
|
|
|
|
|
|
def get_first_unit_name(application_name, model_name=None):
|
|
"""Return name of lowest numbered unit of given application.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:returns: Name of lowest numbered unit
|
|
:rtype: str
|
|
"""
|
|
return get_units(application_name, model_name=model_name)[0].name
|
|
|
|
|
|
def get_app_ips(application_name, model_name=None):
|
|
"""Return public address of all units of an application.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:returns: List of ip addresses
|
|
:rtype: [str, str,...]
|
|
"""
|
|
return [u.public_address
|
|
for u in get_units(application_name, model_name=model_name)]
|
|
|
|
|
|
async def async_get_application_config(application_name, model_name=None):
|
|
"""Return application configuration.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:returns: Dictionary of configuration
|
|
:rtype: dict
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
return await model.applications[application_name].get_config()
|
|
|
|
get_application_config = sync_wrapper(async_get_application_config)
|
|
|
|
|
|
async def async_set_application_config(application_name, configuration,
|
|
model_name=None):
|
|
"""Set application configuration.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:param configuration: Dictionary of configuration setting(s)
|
|
:type configuration: dict
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
return await (model.applications[application_name]
|
|
.set_config(configuration))
|
|
|
|
set_application_config = sync_wrapper(async_set_application_config)
|
|
|
|
|
|
async def async_get_status(model_name=None):
|
|
"""Return full status.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:returns: dictionary of juju status
|
|
:rtype: dict
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
return await model.get_status()
|
|
|
|
get_status = sync_wrapper(async_get_status)
|
|
|
|
|
|
async def async_run_action(unit_name, action_name, model_name=None,
|
|
action_params=None):
|
|
"""Run action on given unit.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param unit_name: Name of unit to run action on
|
|
:type unit_name: str
|
|
:param action_name: Name of action to run
|
|
:type action_name: str
|
|
:param action_params: Dictionary of config options for action
|
|
:type action_params: dict
|
|
:returns: Action object
|
|
:rtype: juju.action.Action
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
unit = get_unit_from_name(unit_name, model)
|
|
action_obj = await unit.run_action(action_name, **action_params)
|
|
await action_obj.wait()
|
|
return action_obj
|
|
|
|
run_action = sync_wrapper(async_run_action)
|
|
|
|
|
|
async def async_run_action_on_leader(application_name, action_name,
|
|
model_name=None, action_params=None):
|
|
"""Run action on lead unit of the given application.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:param action_name: Name of action to run
|
|
:type action_name: str
|
|
:param action_params: Dictionary of config options for action
|
|
:type action_params: dict
|
|
:returns: Action object
|
|
:rtype: juju.action.Action
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
for unit in model.applications[application_name].units:
|
|
is_leader = await unit.is_leader_from_status()
|
|
if is_leader:
|
|
action_obj = await unit.run_action(action_name,
|
|
**action_params)
|
|
await action_obj.wait()
|
|
return action_obj
|
|
|
|
run_action_on_leader = sync_wrapper(async_run_action_on_leader)
|
|
|
|
|
|
class UnitError(Exception):
|
|
"""Exception raised for units in error state."""
|
|
|
|
def __init__(self, units):
|
|
"""Set units in error state in messgae and raise."""
|
|
message = "Units {} in error state".format(
|
|
','.join([u.entity_id for u in units]))
|
|
super(UnitError, self).__init__(message)
|
|
|
|
|
|
class ServiceNotRunning(Exception):
|
|
"""Exception raised when service not running."""
|
|
|
|
def __init__(self, service):
|
|
"""Set not running services in messgae and raise."""
|
|
message = "Service {} not running".format(service)
|
|
super(ServiceNotRunning, self).__init__(message)
|
|
|
|
|
|
def units_with_wl_status_state(model, state):
|
|
"""Return a list of unit which have a matching workload status.
|
|
|
|
:returns: Units in error state
|
|
:rtype: [juju.Unit, ...]
|
|
"""
|
|
matching_units = []
|
|
for unit in model.units.values():
|
|
wl_status = unit.workload_status
|
|
if wl_status == state:
|
|
matching_units.append(unit)
|
|
return matching_units
|
|
|
|
|
|
def check_model_for_hard_errors(model):
|
|
"""Check model for any hard errors that should halt a deployment.
|
|
|
|
The only check currently implemented is checking for units in an
|
|
error state
|
|
|
|
:raises: UnitError
|
|
"""
|
|
errored_units = units_with_wl_status_state(model, 'error')
|
|
if errored_units:
|
|
raise UnitError(errored_units)
|
|
|
|
|
|
def check_unit_workload_status(model, unit, state):
|
|
"""Check that the units workload status matches the supplied state.
|
|
|
|
This function has the side effect of also checking for *any* units
|
|
in an error state and aborting if any are found.
|
|
|
|
:param model: Model object to check in
|
|
:type model: juju.Model
|
|
:param unit: Unit to check wl status of
|
|
:type unit: juju.Unit
|
|
:param state: Expected unit work load state
|
|
:type state: str
|
|
:raises: UnitError
|
|
:returns: Whether units workload status matches desired state
|
|
:rtype: bool
|
|
"""
|
|
check_model_for_hard_errors(model)
|
|
return unit.workload_status == state
|
|
|
|
|
|
def check_unit_workload_status_message(model, unit, message=None,
|
|
prefixes=None):
|
|
"""Check that the units workload status message.
|
|
|
|
Check that the units workload status message matches the supplied
|
|
message or starts with one of the supplied prefixes. Raises an exception
|
|
if neither prefixes or message is set. This function has the side effect
|
|
of also checking for *any* units in an error state and aborting if any
|
|
are found.
|
|
|
|
:param model: Model object to check in
|
|
:type model: juju.Model
|
|
:param unit: Unit to check wl status of
|
|
:type unit: juju.Unit
|
|
:param message: Expected message text
|
|
:type message: str
|
|
:param prefixes: Prefixes to match message against
|
|
:type prefixes: tuple
|
|
:raises: ValueError, UnitError
|
|
:returns: Whether message matches desired string
|
|
:rtype: bool
|
|
"""
|
|
check_model_for_hard_errors(model)
|
|
if message is not None:
|
|
return unit.workload_status_message == message
|
|
elif prefixes is not None:
|
|
return unit.workload_status_message.startswith(prefixes)
|
|
else:
|
|
raise ValueError("Must be called with message or prefixes")
|
|
|
|
|
|
async def async_wait_for_application_states(model_name=None, states=None,
|
|
timeout=2700):
|
|
"""Wait for model to achieve the desired state.
|
|
|
|
Check the workload status and workload status message for every unit of
|
|
every application. By default look for an 'active' workload status and a
|
|
message that starts with one of the approved_message_prefixes.
|
|
|
|
Bespoke statuses and messages can be passed in with states. states takes
|
|
the form::
|
|
|
|
states = {
|
|
'app': {
|
|
'workload-status': 'blocked',
|
|
'workload-status-message': 'No requests without a prod'}
|
|
'anotherapp': {
|
|
'workload-status-message': 'Unit is super ready'}}
|
|
wait_for_application_states('modelname', states=states)
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param states: States to look for
|
|
:type states: dict
|
|
:param timeout: Time to wait for status to be achieved
|
|
:type timeout: int
|
|
"""
|
|
approved_message_prefixes = ('ready', 'Ready', 'Unit is ready')
|
|
|
|
if not states:
|
|
states = {}
|
|
async with run_in_model(model_name) as model:
|
|
check_model_for_hard_errors(model)
|
|
logging.info("Waiting for a unit to appear")
|
|
await model.block_until(
|
|
lambda: len(model.units) > 0
|
|
)
|
|
logging.info("Waiting for all units to be idle")
|
|
await model.block_until(
|
|
lambda: model.all_units_idle(), timeout=timeout)
|
|
for application in model.applications:
|
|
check_info = states.get(application, {})
|
|
for unit in model.applications[application].units:
|
|
logging.info("Checking workload status of {}".format(
|
|
unit.entity_id))
|
|
await model.block_until(
|
|
lambda: check_unit_workload_status(
|
|
model,
|
|
unit,
|
|
check_info.get('workload-status', 'active')),
|
|
timeout=timeout)
|
|
check_msg = check_info.get('workload-status-message')
|
|
logging.info("Checking workload status message of {}".format(
|
|
unit.entity_id))
|
|
if check_msg is not None:
|
|
prefixes = (check_msg)
|
|
else:
|
|
prefixes = approved_message_prefixes
|
|
await model.block_until(
|
|
lambda: check_unit_workload_status_message(
|
|
model,
|
|
unit,
|
|
prefixes=prefixes),
|
|
timeout=timeout)
|
|
|
|
wait_for_application_states = sync_wrapper(async_wait_for_application_states)
|
|
|
|
|
|
async def async_block_until_all_units_idle(model_name=None, timeout=2700):
|
|
"""Block until all units in the given model are idle.
|
|
|
|
An example accessing this function via its sync wrapper::
|
|
|
|
block_until_all_units_idle('modelname')
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param timeout: Time to wait for status to be achieved
|
|
:type timeout: float
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
await model.block_until(
|
|
lambda: model.all_units_idle(), timeout=timeout)
|
|
|
|
block_until_all_units_idle = sync_wrapper(async_block_until_all_units_idle)
|
|
|
|
|
|
async def async_block_until_service_status(unit_name, services, target_status,
|
|
model_name=None, timeout=2700):
|
|
"""Block until all services on the unit are in the desired state.
|
|
|
|
Block until all services on the unit are in the desired state (stopped
|
|
or running)::
|
|
|
|
block_until_service_status(
|
|
'modelname',
|
|
first_unit,
|
|
['glance-api'],
|
|
'running')
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param unit_name: Name of unit to run action on
|
|
:type unit_name: str
|
|
:param services: List of services to check
|
|
:type services: []
|
|
:param target_status: State services should be in (stopped or running)
|
|
:type target_status: str
|
|
:param timeout: Time to wait for status to be achieved
|
|
:type timeout: int
|
|
"""
|
|
async def _check_service():
|
|
for service in services:
|
|
out = await async_run_on_unit(
|
|
model_name,
|
|
unit_name,
|
|
"pidof -x {}".format(service),
|
|
timeout=timeout)
|
|
response_size = len(out['Stdout'].strip())
|
|
if target_status == "running" and response_size == 0:
|
|
return False
|
|
elif target_status == "stopped" and response_size > 0:
|
|
return False
|
|
return True
|
|
async with run_in_model(model_name):
|
|
await async_block_until(_check_service, timeout=timeout)
|
|
|
|
block_until_service_status = sync_wrapper(async_block_until_service_status)
|
|
|
|
|
|
def get_actions(application_name, model_name=None):
|
|
"""Get the actions an applications supports.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:returns: Dictionary of actions and their descriptions
|
|
:rtype: dict
|
|
"""
|
|
if not model_name:
|
|
model_name = get_juju_model()
|
|
# libjuju has not implemented get_actions yet
|
|
# https://github.com/juju/python-libjuju/issues/226
|
|
cmd = ['juju', 'actions', '-m', model_name, application_name,
|
|
'--format', 'yaml']
|
|
return yaml.load(subprocess.check_output(cmd))
|
|
|
|
|
|
async def async_get_current_model():
|
|
"""Return the current active model name.
|
|
|
|
Connect to the current active model and return its name.
|
|
|
|
:returns: String curenet model name
|
|
:rtype: str
|
|
"""
|
|
model = Model()
|
|
await model.connect()
|
|
model_name = model.info.name
|
|
await model.disconnect()
|
|
return model_name
|
|
|
|
|
|
get_current_model = sync_wrapper(async_get_current_model)
|
|
|
|
|
|
async def async_block_until(*conditions, timeout=None, wait_period=0.5,
|
|
loop=None):
|
|
"""Return only after all async conditions are true.
|
|
|
|
Based on juju.utils.block_until which currently does not support
|
|
async methods as conditions.
|
|
|
|
:param conditions: Functions to evaluate.
|
|
:type conditions: functions
|
|
:param timeout: Timeout in seconds
|
|
:type timeout: float
|
|
:param wait_period: Time to wait between re-assessing conditions.
|
|
:type wait_period: float
|
|
:param loop: The event loop to use
|
|
:type loop: An event loop
|
|
"""
|
|
async def _block():
|
|
while True:
|
|
evaluated = []
|
|
for c in conditions:
|
|
result = await c()
|
|
evaluated.append(result)
|
|
if all(evaluated):
|
|
return
|
|
else:
|
|
await asyncio.sleep(wait_period, loop=loop)
|
|
await asyncio.wait_for(_block(), timeout, loop=loop)
|
|
|
|
|
|
async def async_block_until_file_ready(application_name, remote_file,
|
|
check_function, model_name=None,
|
|
timeout=2700):
|
|
"""Block until the check_function passes against.
|
|
|
|
Block until the check_function passes against the provided file. It is
|
|
unlikely that a test would call this function directly, rather it is
|
|
provided as scaffolding for tests with a more specialised purpose.
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:param remote_file: Remote path of file to transfer
|
|
:type remote_file: str
|
|
:param check_function: Function to use to check if file is ready, must take
|
|
exactly one argument which is the file contents.
|
|
:type check_function: function
|
|
:param timeout: Time to wait for contents to appear in file
|
|
:type timeout: float
|
|
"""
|
|
async def _check_file():
|
|
file_name = os.path.basename(remote_file)
|
|
units = model.applications[application_name].units
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
for unit in units:
|
|
try:
|
|
await unit.scp_from(remote_file, tmpdir)
|
|
with open(os.path.join(tmpdir, file_name), 'r') as lf:
|
|
contents = lf.read()
|
|
if not check_function(contents):
|
|
return False
|
|
# libjuju throws a generic error for scp failure. So we cannot
|
|
# differentiate between a connectivity issue and a target file
|
|
# not existing error. For now just assume the latter.
|
|
except JujuError as e:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
async with run_in_model(model_name) as model:
|
|
await async_block_until(_check_file, timeout=timeout)
|
|
|
|
|
|
async def async_block_until_file_has_contents(application_name, remote_file,
|
|
expected_contents,
|
|
model_name=None, timeout=2700):
|
|
"""Block until the expected_contents are present on all units.
|
|
|
|
Block until the given string (expected_contents) is present in the file
|
|
(remote_file) on all units of the given application.
|
|
|
|
An example accessing this function via its sync wrapper::
|
|
|
|
block_until_file_has_contents(
|
|
'modelname'
|
|
'keystone',
|
|
'/etc/apache2/apache2.conf',
|
|
'KeepAlive On')
|
|
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:param remote_file: Remote path of file to transfer
|
|
:type remote_file: str
|
|
:param expected_contents: String to look for in file
|
|
:type expected_contents: str
|
|
:param timeout: Time to wait for contents to appear in file
|
|
:type timeout: float
|
|
"""
|
|
def f(x):
|
|
return expected_contents in x
|
|
return await async_block_until_file_ready(
|
|
application_name,
|
|
remote_file,
|
|
f,
|
|
timeout=timeout,
|
|
model_name=model_name)
|
|
|
|
block_until_file_has_contents = sync_wrapper(
|
|
async_block_until_file_has_contents)
|
|
|
|
|
|
async def async_block_until_oslo_config_entries_match(application_name,
|
|
remote_file,
|
|
expected_contents,
|
|
model_name=None,
|
|
timeout=2700):
|
|
"""Block until dict is represented in the file using oslo.config parser.
|
|
|
|
Block until the expected_contents are in the given file on all units of
|
|
the application. For example to check for the following configuration::
|
|
|
|
[DEFAULT]
|
|
debug = False
|
|
|
|
[glance_store]
|
|
filesystem_store_datadir = /var/lib/glance/images/
|
|
default_store = file
|
|
|
|
Call the check via its sync wrapper::
|
|
|
|
expected_contents = {
|
|
'DEFAULT': {
|
|
'debug': ['False']},
|
|
'glance_store': {
|
|
'filesystem_store_datadir': ['/var/lib/glance/images/'],
|
|
'default_store': ['file']}}
|
|
|
|
block_until_oslo_config_entries_match(
|
|
'modelname',
|
|
'glance',
|
|
'/etc/glance/glance-api.conf',
|
|
expected_contents)
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:param remote_file: Remote path of file to transfer
|
|
:type remote_file: str
|
|
:param expected_contents: The key value pairs in their corresponding
|
|
sections to be looked for in the remote_file
|
|
:type expected_contents: {}
|
|
:param timeout: Time to wait for contents to appear in file
|
|
:type timeout: float
|
|
|
|
"""
|
|
def f(x):
|
|
# Writing out the file that was just read is suboptimal
|
|
with tempfile.NamedTemporaryFile(mode='w', delete=True) as fp:
|
|
fp.write(x)
|
|
fp.seek(0)
|
|
sections = {}
|
|
parser = cfg.ConfigParser(fp.name, sections)
|
|
parser.parse()
|
|
for section, entries in expected_contents.items():
|
|
for key, value in entries.items():
|
|
if sections.get(section, {}).get(key) != value:
|
|
return False
|
|
return True
|
|
return await async_block_until_file_ready(
|
|
application_name,
|
|
remote_file,
|
|
f,
|
|
timeout=timeout,
|
|
model_name=model_name)
|
|
|
|
|
|
block_until_oslo_config_entries_match = sync_wrapper(
|
|
async_block_until_oslo_config_entries_match)
|
|
|
|
|
|
async def async_block_until_services_restarted(application_name, mtime,
|
|
services, model_name=None,
|
|
timeout=2700):
|
|
"""Block until the given services have a start time later then mtime.
|
|
|
|
For example to check that the glance-api service has been restarted::
|
|
|
|
block_until_services_restarted(
|
|
'modelname'
|
|
'glance',
|
|
1528294585,
|
|
['glance-api'])
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param application_name: Name of application
|
|
:type application_name: str
|
|
:param mtime: Time in seconds since Epoch to check against
|
|
:type mtime: int
|
|
:param services: Listr of services to check restart time of
|
|
:type services: []
|
|
:param timeout: Time to wait for services to be restarted
|
|
:type timeout: float
|
|
"""
|
|
async def _check_service():
|
|
units = model.applications[application_name].units
|
|
for unit in units:
|
|
for service in services:
|
|
try:
|
|
svc_mtime = await async_get_unit_service_start_time(
|
|
model_name,
|
|
unit.entity_id,
|
|
service)
|
|
except ServiceNotRunning:
|
|
return False
|
|
if svc_mtime < mtime:
|
|
return False
|
|
return True
|
|
async with run_in_model(model_name) as model:
|
|
await async_block_until(_check_service, timeout=timeout)
|
|
|
|
|
|
block_until_services_restarted = sync_wrapper(
|
|
async_block_until_services_restarted)
|
|
|
|
|
|
async def async_block_until_unit_wl_status(unit_name, status, model_name=None,
|
|
timeout=2700):
|
|
"""Block until the given unit has the desired workload status.
|
|
|
|
A units workload status may change during a given action. This function
|
|
blocks until the given unit has the desired workload status::
|
|
|
|
block_until_unit_wl_status(
|
|
'modelname',
|
|
aunit,
|
|
'active')
|
|
|
|
:param model_name: Name of model to query.
|
|
:type model_name: str
|
|
:param unit_name: Name of unit to run action on
|
|
:type unit_name: str
|
|
:param status: Status to wait for (active, maintenance etc)
|
|
:type status: str
|
|
:param timeout: Time to wait for unit to achieved desired status
|
|
:type timeout: float
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
unit = get_unit_from_name(unit_name, model)
|
|
await model.block_until(
|
|
lambda: unit.workload_status == status,
|
|
timeout=timeout
|
|
)
|
|
|
|
block_until_unit_wl_status = sync_wrapper(
|
|
async_block_until_unit_wl_status)
|
|
|
|
|
|
async def async_get_relation_id(application_name, remote_application_name,
|
|
model_name=None,
|
|
remote_interface_name=None):
|
|
"""
|
|
Get relation id of relation from model.
|
|
|
|
:param model_name: Name of model to operate on
|
|
:type model_name: str
|
|
:param application_name: Name of application on this side of relation
|
|
:type application_name: str
|
|
:param remote_application_name: Name of application on other side of
|
|
relation
|
|
:type remote_application_name: str
|
|
:param remote_interface_name: Name of interface on remote end of relation
|
|
:type remote_interface_name: Optional(str)
|
|
:returns: Relation id of relation if found or None
|
|
:rtype: any
|
|
"""
|
|
async with run_in_model(model_name) as model:
|
|
for rel in model.applications[application_name].relations:
|
|
spec = '{}'.format(remote_application_name)
|
|
if remote_interface_name is not None:
|
|
spec += ':{}'.format(remote_interface_name)
|
|
if rel.matches(spec):
|
|
return(rel.id)
|
|
|
|
get_relation_id = sync_wrapper(async_get_relation_id)
|
|
|
|
|
|
def set_model_constraints(constraints, model_name=None):
|
|
"""
|
|
Set constraints on a model.
|
|
|
|
Note: Subprocessing out to 'juju' is a temporary solution until
|
|
https://bit.ly/2ujbVPA lands in libjuju.
|
|
|
|
:param model_name: Name of model to operate on
|
|
:type model_name: str
|
|
:param constraints: Constraints to be applied to model
|
|
:type constraints: dict
|
|
|
|
"""
|
|
if not constraints:
|
|
return
|
|
if not model_name:
|
|
model_name = get_juju_model()
|
|
cmd = ['juju', 'set-model-constraints', '-m', model_name]
|
|
cmd.extend(['{}={}'.format(k, v) for k, v in constraints.items()])
|
|
subprocess.check_call(cmd)
|