Added assess_status() functionality - no tests yet
This adds the assess_status functionality, but without any tests yet. It also adds a register function to enable the release to be specified before an OpenStackCharm class is instantiated, thus providing the mechanism to choose the class to use for a release (or series of releases). This was missing from earlier commits.
This commit is contained in:
parent
7bb4d57eeb
commit
659403d17c
|
@ -8,6 +8,7 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import contextlib
|
import contextlib
|
||||||
import collections
|
import collections
|
||||||
|
import itertools
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
@ -34,6 +35,11 @@ _releases = {}
|
||||||
# hook invocation.
|
# hook invocation.
|
||||||
_singleton = None
|
_singleton = None
|
||||||
|
|
||||||
|
# `_release_selector_function` holds a function that takes optionally takes a
|
||||||
|
# release and commutes it to another release or just returns a release.
|
||||||
|
# This is to enable the defining code to define which release is used.
|
||||||
|
_release_selector_function = None
|
||||||
|
|
||||||
# List of releases that OpenStackCharm based charms know about
|
# List of releases that OpenStackCharm based charms know about
|
||||||
KNOWN_RELEASES = [
|
KNOWN_RELEASES = [
|
||||||
'diablo',
|
'diablo',
|
||||||
|
@ -75,6 +81,10 @@ def get_charm_instance(release=None, *args, **kwargs):
|
||||||
"Release {} is not supported by this charm. Earliest support is "
|
"Release {} is not supported by this charm. Earliest support is "
|
||||||
"{} release".format(release, known_releases[0]))
|
"{} release".format(release, known_releases[0]))
|
||||||
else:
|
else:
|
||||||
|
# check that the release is a valid release
|
||||||
|
if release not in KNOWN_RELEASES:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Release {} is not a known OpenStack release?".format(release))
|
||||||
# try to find the release that is supported.
|
# try to find the release that is supported.
|
||||||
for known_release in reversed(known_releases):
|
for known_release in reversed(known_releases):
|
||||||
if release >= known_release:
|
if release >= known_release:
|
||||||
|
@ -85,6 +95,30 @@ def get_charm_instance(release=None, *args, **kwargs):
|
||||||
return cls(release=release, *args, **kwargs)
|
return cls(release=release, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def register_os_release_selector(f):
|
||||||
|
"""Register a function that determines what the release is for the
|
||||||
|
invocation run. This allows the charm to define HOW the release is
|
||||||
|
determined.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
@register_os_release_selector
|
||||||
|
def my_release_selector():
|
||||||
|
return os_release_chooser()
|
||||||
|
|
||||||
|
The function should return a string which is an OS release.
|
||||||
|
"""
|
||||||
|
global _release_selector_function
|
||||||
|
if _release_selector_function is None:
|
||||||
|
# we can only do this once in a system invocation.
|
||||||
|
_release_selector_function = f
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Only a single release_selector_function is supported."
|
||||||
|
" Called with {}".format(f.__name__))
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
class OpenStackCharmMeta(type):
|
class OpenStackCharmMeta(type):
|
||||||
"""Metaclass to provide a classproperty of 'singleton' so that class
|
"""Metaclass to provide a classproperty of 'singleton' so that class
|
||||||
methods in the derived OpenStackCharm() class can simply use cls.singleton
|
methods in the derived OpenStackCharm() class can simply use cls.singleton
|
||||||
|
@ -141,8 +175,17 @@ class OpenStackCharmMeta(type):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def singleton(cls):
|
def singleton(cls):
|
||||||
|
"""Either returns the already created charm, or create a new one.
|
||||||
|
|
||||||
|
This uses the _release_selector_function to choose the release is one
|
||||||
|
has been registered, otherwise None is passed to get_charm_instance()
|
||||||
|
"""
|
||||||
global _singleton
|
global _singleton
|
||||||
if _singleton is None:
|
if _singleton is None:
|
||||||
|
release = None
|
||||||
|
# see if a _release_selector_function has been registered.
|
||||||
|
if _release_selector_function is not None:
|
||||||
|
release = _release_selector_function()
|
||||||
_singleton = get_charm_instance()
|
_singleton = get_charm_instance()
|
||||||
return _singleton
|
return _singleton
|
||||||
|
|
||||||
|
@ -187,6 +230,10 @@ class OpenStackCharm(object):
|
||||||
# }
|
# }
|
||||||
restart_map = {}
|
restart_map = {}
|
||||||
|
|
||||||
|
# The list of required services that are checked for assess_status
|
||||||
|
# e.g. required_relations = ['identity-service', 'shared-db']
|
||||||
|
required_relations = []
|
||||||
|
|
||||||
# The command used to sync the database
|
# The command used to sync the database
|
||||||
sync_cmd = []
|
sync_cmd = []
|
||||||
|
|
||||||
|
@ -226,11 +273,9 @@ class OpenStackCharm(object):
|
||||||
if packages:
|
if packages:
|
||||||
hookenv.status_set('maintenance', 'Installing packages')
|
hookenv.status_set('maintenance', 'Installing packages')
|
||||||
charmhelpers.fetch.apt_install(packages, fatal=True)
|
charmhelpers.fetch.apt_install(packages, fatal=True)
|
||||||
# TODO need a call to assess_status(...) or equivalent so that we
|
|
||||||
# can determine the workload status at the end of the handler. At
|
|
||||||
# the end of install the 'status' is stuck in maintenance until the
|
|
||||||
# next hook is run.
|
|
||||||
self.set_state('{}-installed'.format(self.name))
|
self.set_state('{}-installed'.format(self.name))
|
||||||
|
hookenv.status_set('maintenance',
|
||||||
|
'Installation complete - awaiting next status')
|
||||||
|
|
||||||
def set_state(self, state, value=None):
|
def set_state(self, state, value=None):
|
||||||
"""proxy for charms.reactive.bus.set_state()"""
|
"""proxy for charms.reactive.bus.set_state()"""
|
||||||
|
@ -379,3 +424,159 @@ class OpenStackCharm(object):
|
||||||
# Restart services immediatly after db sync as
|
# Restart services immediatly after db sync as
|
||||||
# render_domain_config needs a working system
|
# render_domain_config needs a working system
|
||||||
self.restart_all()
|
self.restart_all()
|
||||||
|
|
||||||
|
def assess_status(self):
|
||||||
|
"""Assess the status of the unit and set the status and a useful
|
||||||
|
message as appropriate.
|
||||||
|
|
||||||
|
The 3 checks are:
|
||||||
|
|
||||||
|
1. Check if the unit has been paused (using
|
||||||
|
os_utils.is_unit_paused_set().
|
||||||
|
2. Check if the interfaces are all present (using the states that are
|
||||||
|
set by each interface as it comes 'live'.
|
||||||
|
3. Do a custom_assess_status_check() check.
|
||||||
|
4. Check that services that should be running are running.
|
||||||
|
|
||||||
|
Each sub-function determins what checks are taking place.
|
||||||
|
|
||||||
|
If custom assess_status() functionality is required then the derived
|
||||||
|
class should override any of the 4 check functions to alter the
|
||||||
|
behaviour as required.
|
||||||
|
|
||||||
|
Note that if ports are NOT to be checked, then the derived class should
|
||||||
|
override :meth:`ports_to_check()` and return an empty list.
|
||||||
|
|
||||||
|
SIDE EFFECT: this function calls status_set(state, message) to set the
|
||||||
|
workload status in juju.
|
||||||
|
"""
|
||||||
|
for f in [self.check_if_paused,
|
||||||
|
self.check_interfaces,
|
||||||
|
self.custom_assess_status_check,
|
||||||
|
self.check_services_running]:
|
||||||
|
state, message = f()
|
||||||
|
if state is not None:
|
||||||
|
hookenv.status_set(state, message)
|
||||||
|
return
|
||||||
|
# No state was particularly set, so assume the unit is active
|
||||||
|
hookenv.state_set('active', 'Unit is ready')
|
||||||
|
|
||||||
|
def custom_assess_status_check(self):
|
||||||
|
"""Override this function in a derived class if there are any other
|
||||||
|
status checks that need to be done that aren't about relations, etc.
|
||||||
|
|
||||||
|
Return (None, None) if the status is okay (i.e. the unit is active).
|
||||||
|
Return ('active', message) do shortcut and force the unit to the active
|
||||||
|
status.
|
||||||
|
Return (other_status, message) to set the status to desired state.
|
||||||
|
|
||||||
|
:returns: None, None - no action in this function.
|
||||||
|
"""
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def check_if_paused(self):
|
||||||
|
"""Check if the unit is paused and return either the paused status,
|
||||||
|
message or None, None if the unit is not paused. If the unit is paused
|
||||||
|
but a service is incorrectly running, then the function returns a
|
||||||
|
broken status.
|
||||||
|
|
||||||
|
:returns: (status, message) or (None, None)
|
||||||
|
"""
|
||||||
|
return os_utils._ows_check_if_paused(
|
||||||
|
services=self.services,
|
||||||
|
ports=self.ports_to_check(self.api_ports))
|
||||||
|
|
||||||
|
def check_interfaces(self):
|
||||||
|
"""Check that the required interfaces have both connected and availble
|
||||||
|
states set.
|
||||||
|
|
||||||
|
This requires a convention from the OS interfaces that they set the
|
||||||
|
'{relation_name}.connected' state on connection, and the
|
||||||
|
'{relation_name}.available' state when the connection information is
|
||||||
|
available and the interface is ready to go.
|
||||||
|
|
||||||
|
The interfaces (relations) that are checked are named in
|
||||||
|
self.required_relations which is a list of strings representing the
|
||||||
|
generic relation name. e.g. 'identity-service' rather than 'keystone'.
|
||||||
|
|
||||||
|
Returns (None, None) if the interfaces are okay, or a status, message
|
||||||
|
if any of the interfaces are not ready.
|
||||||
|
|
||||||
|
Derived classes can augment/alter the checks done by overriding the
|
||||||
|
companion method :property:`states_to_check` which converts a relation
|
||||||
|
into the states to confirm existence, along with the error message.
|
||||||
|
|
||||||
|
:returns (status, message) or (None, None)
|
||||||
|
"""
|
||||||
|
states_to_check = self.states_to_check
|
||||||
|
# bail if there is nothing to do.
|
||||||
|
if not states_to_check:
|
||||||
|
return None, None
|
||||||
|
available_states = charms.reactive.bus.get_states().keys()
|
||||||
|
status = None
|
||||||
|
messages = []
|
||||||
|
for relation, states in states_to_check.items():
|
||||||
|
for state, err_status, err_msg in states:
|
||||||
|
if state not in available_states:
|
||||||
|
messages.append(err_msg)
|
||||||
|
status = os_utils.workload_state_compare(status,
|
||||||
|
err_status)
|
||||||
|
# as soon as we error on a relation, skip to the next one.
|
||||||
|
break
|
||||||
|
if status is not None:
|
||||||
|
return status, ", ".join(messages)
|
||||||
|
# Everything is fine.
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def states_to_check(self):
|
||||||
|
"""Construct a default set of connected and available states for each
|
||||||
|
of the relations passed, along with error messages and new status
|
||||||
|
conditions if they are missing.
|
||||||
|
|
||||||
|
The method returns a {relation: [(state, err_status, err_msg), (...),]}
|
||||||
|
This corresponds to the relation, the state to check for, the error
|
||||||
|
status to set if that state is missing, and the message to show if the
|
||||||
|
state is missing.
|
||||||
|
|
||||||
|
The list of tuples is evaulated in order for each relation, and stops
|
||||||
|
after the first failure. This means that it doesn't check (say)
|
||||||
|
available if connected is not available.
|
||||||
|
"""
|
||||||
|
states_to_check = {
|
||||||
|
relation: [("{}.connected".format(relation),
|
||||||
|
"blocked",
|
||||||
|
"'{}' missing".format(relation)),
|
||||||
|
("{}.available".format(relation),
|
||||||
|
"waiting",
|
||||||
|
"'{}' incomplete".format(relation))]
|
||||||
|
for relation in self.required_relations}
|
||||||
|
return states_to_check
|
||||||
|
|
||||||
|
def check_services_running(self):
|
||||||
|
"""Check that the services that should be running are actually running.
|
||||||
|
|
||||||
|
This uses the self.services and self.api_ports to determine what should
|
||||||
|
be checked.
|
||||||
|
|
||||||
|
:returns: (status, message) or (None, None).
|
||||||
|
"""
|
||||||
|
# This returns either a None, None or a status, message if the service
|
||||||
|
# is not running or the ports are not open.
|
||||||
|
return os_utils._ows_check_services_running(
|
||||||
|
services=self.services,
|
||||||
|
ports=self.ports_to_check(self.api_ports))
|
||||||
|
|
||||||
|
def ports_to_check(self, ports):
|
||||||
|
"""Return a flattened, sorted, unique list of ports from self.api_ports
|
||||||
|
|
||||||
|
NOTE. To disable port checking, simply override this method in the
|
||||||
|
derived class and return an empty [].
|
||||||
|
|
||||||
|
:param ports: {key: {subkey: value}}
|
||||||
|
:returns: [value1, value2, ...]
|
||||||
|
"""
|
||||||
|
# NB self.api_ports = {key: {space: value}}
|
||||||
|
# The chain .. map flattens all the values into a single list
|
||||||
|
return sorted(set(itertools.chain(*map(lambda x: x.values(),
|
||||||
|
self.api_ports.values()))))
|
||||||
|
|
Loading…
Reference in New Issue