Files
zaza-openstack-tests/zaza/model.py
David Ames ea120ee0a5 Add controller module
The controller module allows us to run commands against the controller
including listing all models and getting the cloud configuration name.
2018-05-14 10:49:49 -07:00

665 lines
22 KiB
Python

import asyncio
from async_generator import async_generator, yield_, asynccontextmanager
import logging
import os
import subprocess
import tempfile
import yaml
from juju import loop
from juju.errors import JujuError
from juju.model import Model
from zaza import sync_wrapper
async def deployed(filter=None):
# 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()
await model.connect_model(model_name)
await yield_(model)
await model.disconnect()
async def async_scp_to_unit(model_name, unit_name, source, destination,
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(model_name, application_name, source,
destination, 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(model_name, unit_name, source, destination,
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(model_name, unit_name, command, 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: DISABLED due to Issue #225
https://github.com/juju/python-libjuju/issues/225
:type timeout: int
:returns: action.data['results'] {'Code': '', 'Stderr': '', 'Stdout': ''}
:rtype: dict
"""
# Disabling timeout due to Issue #225
# https://github.com/juju/python-libjuju/issues/225
if timeout:
timeout = None
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_application(model_name, application_name):
"""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: Appliction 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(model_name, application_name):
"""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(model_name, application_name):
"""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(model_name, application_name):
"""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(model_name, application_name)[0].name
def get_app_ips(model_name, application_name):
"""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(model_name, application_name)]
async def async_get_application_config(model_name, application_name):
"""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(model_name, application_name,
configuration):
"""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
:returns: None
:rtype: None
"""
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):
"""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(model_name, unit_name, action_name,
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(model_name, application_name, action_name,
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):
message = "Units {} in error state".format(
','.join([u.entity_id for u in units]))
super(UnitError, 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 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:
return unit.workload_status_message == message
elif prefixes:
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, 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:
{
'app': {
'workload-status': 'blocked',
'workload-status-message': 'No requests without a prod'}
'anotherapp': {
'workload-status-message': 'Unit is super ready'}}
:param model_name: Name of model to query.
:type model_name: str
:param states: Staes 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:
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)
def get_actions(model_name, application_name):
"""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
"""
# 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 secounds
:type timeout: float
:param wait_period: Time to wait between re-assing conditions.
:type wait_period: float
:param loop: The evnt 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(model_name, application_name,
remote_file, check_function,
timeout=2700):
"""Block until the check_function passes against the contents of the given
file on all units of the application
: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(model_name, application_name,
remote_file, expected_contents,
timeout=2700):
"""Block until the expected_contents are in the given file on all units of
the application
: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(model_name, application_name,
remote_file, f, timeout)
block_until_file_has_contents = sync_wrapper(
async_block_until_file_has_contents)
def main():
# Run the deploy coroutine in an asyncio event loop, using a helper
# that abstracts loop creation and teardown.
print("Current applications: {}".format(", ".join(loop.run(deployed()))))
if __name__ == '__main__':
main()