Added leadership to implicit selectors. #11

Merged
merged 8 commits into from Nov 22, 2016
@@ -95,7 +95,12 @@ def decorate(self, func):
async def kill_juju_agent(rule: Rule, model: Model, unit: Unit):
"""Kill the juju agent on a machine."""
- await unit.run('sudo pkill jujud')
+ try:
+ await unit.run('sudo pkill jujud')
+ except AttributeError:
+ # We kill the juju agent, so we will get an Exception back
+ # from unit.run.
+ pass
# TODO: implement these actions:
@@ -21,7 +21,7 @@ def default_resolver(model, kind, name):
return obj
-def select(model, selectors, objects=None, resolver=default_resolver):
+async def select(rule, model, selectors, objects=None, resolver=default_resolver):
if not selectors:
if objects is None:
raise ValueError('No valid objects specified by selectors')
@@ -31,7 +31,7 @@ def select(model, selectors, objects=None, resolver=default_resolver):
# example) we must resolve them relative to the current model. This is
# pluggable using a resolver object which takes a model,
cur = None
- args = [model]
+ args = [rule, model]
# This can raise many an exception
for selector in selectors:
data = selector.copy()
@@ -43,10 +43,10 @@ def select(model, selectors, objects=None, resolver=default_resolver):
if o is not None:
data[k] = o
- cur = m(*args, **data)
+ cur = await m(*args, **data)
if len(cur) < 1: # If we get an empty list ...
return cur # ... return it, and skip the rest.
- args = [model, cur]
+ args = [rule, model, cur]
return cur
@@ -71,7 +71,10 @@ def select(model, selectors, objects=None, resolver=default_resolver):
glitch_plan = validate_plan(yaml.load(f))
rule.log.info("loaded glitch plan from {}".format(config.glitch_plan))
else:
- glitch_plan = generate_plan(model, num=int(config.glitch_num))
+ glitch_plan = await generate_plan(
+ rule,
+ model,
+ num=int(config.glitch_num))
glitch_plan = validate_plan(glitch_plan)
rule.log.info("Writing glitch plan to {}".format(config.glitch_output))
@@ -83,7 +86,7 @@ def select(model, selectors, objects=None, resolver=default_resolver):
actionf = Actions[action.pop('action')]['func']
selectors = action.pop('selectors')
# Find a set of units to act upon
- objects = select(model, selectors)
+ objects = await select(rule, model, selectors)
if not objects:
# If we get an empty set of objects back, just skip this action.
rule.log.error(
@@ -94,9 +97,6 @@ def select(model, selectors, objects=None, resolver=default_resolver):
# Run the specified action on those units
rule.log.debug("GLITCHING {}: {}".format(actionf.__name__, action))
- # TODO: better handle the case where we no longer have objects
- # to select, due to too many of them being destroyed (most
- # relevant for small bundles)
await actionf(rule, model, objects, **action)
context.bus.dispatch(
origin="glitch",
@@ -3,54 +3,67 @@
from .selectors import Selectors
from .actions import Actions
+
class InvalidPlan(Exception):
pass
+
class InvalidModel(Exception):
pass
-_MODEL_OPS = {
- 'machine': {
- 'fetch': lambda model: [m for m in model.machines.values()],
- 'selectors': lambda m: [
- {'selector': 'machines'}, {'selector': 'one'}],
- },
- 'unit': {
- 'fetch': lambda model: [u for u in model.units.values()],
- 'selectors': lambda u: [
- {'selector': 'units', 'application': u.application},
- # TODO: handle leadership
- {'selector': 'one'}
- ]
- },
- 'application': {
- 'fetch': lambda model: [a for a in model.applications.values()],
- 'selectors': lambda a: [
- {'selector': 'applications'},
- {'selector': 'one'}
- ]
- }
-}
-
-
-def _fetch_objects(object_type, model):
- try:
- func = _MODEL_OPS[object_type]['fetch']
- except KeyError:
- raise InvalidPlan("Could not fetch objects of {}".format(object_type))
- objects = func(model)
- if not objects:
- raise InvalidModel("No objects to test in the current model")
- return objects
-
-
-def _implicit_selectors(object_type, obj):
- try:
- func = _MODEL_OPS[object_type]['selectors']
- except KeyError:
- raise InvalidPlan("Could not get implicit selectors for {}".format(object_type))
- return func(obj)
+async def _fetch_machine(rule, model):
+ machines = [m for m in model.machines.values()]
+ if not machines:
+ raise InvalidModel("No machines in the model.")
+
+ machine = random.choice(machines)
+
+ selectors = [
+ {'selector': 'machines'},
+ {'selector': 'one'},
+ ]
+ return selectors
+
+
+async def _fetch_unit(rule, model):
+ units = [u for u in model.units.values()]
+ if not units:
+ raise InvalidModel("No units in the model.")
+
+ unit = random.choice(units)
+
+ leadership = await unit.is_leader_from_status()
+
+ selectors = [
+ {'selector': 'units', 'application': unit.application},
+ {'selector': 'leader', 'value': leadership},
+ {'selector': 'one'},
+ ]
+ return selectors
+
+
+async def _fetch_application(rule, model):
+ apps = [a for a in model.applications.values()],
+
+ if not apps:
+ raise InvalidModel("No apps in the model.")
+ app = random.choice(apps)
+
+ selectors = [
+ {'selector': 'applications'},
+ {'selector': 'one'},
+ ]
+ return selectors
+
+
+async def fetch(rule, object_type, model):
+ if object_type == 'machine':
+ return await _fetch_machine(rule, model)
+ if object_type == 'unit':
+ return await _fetch_unit(rule, model)
+ if object_type == 'application':
+ return await _fetch_application(rule, model)
def validate_plan(plan):
@@ -72,7 +85,7 @@ def validate_plan(plan):
return plan
-def generate_plan(model, num):
+async def generate_plan(rule, model, num):
'''
Generate a test plan. The resultant plan, if written out to a
.yaml file, would look something like the following:
@@ -104,9 +117,7 @@ def generate_plan(model, num):
action = random.choice([a for a in Actions])
obj_type = Actions[action]['type']
- objects = _fetch_objects(obj_type, model)
- obj = random.choice(objects)
- selectors = _implicit_selectors(obj_type, obj)
+ selectors = await fetch(rule, obj_type, model)
plan['actions'].append({'action': action, 'selectors': selectors})
@@ -9,12 +9,15 @@
from juju.machine import Machine
from juju.unit import Unit
+from matrix.model import Rule
from matrix.utils import Singleton
_marker = object()
log = logging.getLogger("glitch")
+class SelectError(Exception): pass
+
class _Selectors(dict, metaclass=Singleton):
def decorate(self, f):
@@ -36,7 +39,7 @@ def decorate(self, f):
@selector
-def units(model: Model, application: Application=None) -> List[Unit]:
+async def units(rule: Rule, model: Model, application: Application=None):
"""
Return units that are part of the specified application(s).
@@ -56,30 +59,37 @@ def units(model: Model, application: Application=None) -> List[Unit]:
@selector
-def machines(model: Model) -> List[Machine]:
+async def machines(rule: Rule, model: Model):
machines = [m for m in model.machines.values()]
return machines
@selector
-def applications(model: Model) -> List[Application]:
+async def applications(rule: Rule, model: Model):
return [a for a in model.applications.values()]
-#@selector
-def leader(model: Model, units: List[Unit], value=True) -> List[Unit]:
+@selector
+async def leader(rule: Rule, model: Model, units: List[Unit], value=True):
"""
Return just the units that are, or are not the leader, depending
on whether 'value' is truthy or falsy.
- TODO: fix this to actually check for leadership.
-
"""
- return [u for u in units if u.is_leader is value]
+ passed = []
+
+ for unit in units:
+ if await unit.is_leader_from_status():
+ passed.append(unit)
+
+ # Return our list of leaders or not leaders. If value is True,
+ # this list should be of length one, but this selector does not
+ # take responsibility for checking for that.
+ return passed
@selector
-def agent_status(model: Model, units: List[Unit], expect) -> List[Unit]:
+async def agent_status(rule: Rule, model: Model, units: List[Unit], expect):
'''
Return units with an agent status matching a string.
@@ -88,7 +98,7 @@ def agent_status(model: Model, units: List[Unit], expect) -> List[Unit]:
@selector
-def workload_status(model: Model, units: List[Unit], expect=None) -> List[Unit]:
+async def workload_status(rule: Rule, model: Model, units: List[Unit], expect=None):
"""
Return units with a workload status matching a string.
@@ -97,7 +107,7 @@ def workload_status(model: Model, units: List[Unit], expect=None) -> List[Unit]:
@selector
-def health(units: List[Unit]):
+async def health(units: List[Unit]):
""""
Placeholder for eventual health check selector.
@@ -106,16 +116,13 @@ def health(units: List[Unit]):
@selector
-def one(model: Model, objects: List[Any]) -> List[Any]:
+async def one(rule: Rule, model: Model, objects: List[Any]):
"""
Return just one of a set of units.
The theory is that, whenever we call this, any of the units will
do, so we select a unit at rmandom, to avoid biases introduced by
just selecting the first unit in the list.
- # TODO: it looks like this can except an empty list. Need to
- # refactor to hanle that.
-
"""
return [random.choice(objects)]
@@ -4,4 +4,4 @@ tests:
description: Glitch an already deployed bundle
rules:
- do:
- action: matrix.tasks.glitch
+ task: matrix.tasks.glitch
Binary file not shown.