127 lines
6.1 KiB
Bash
127 lines
6.1 KiB
Bash
nodes_wait 60 || true
|
|
# ans CMD: run CMD as greg in his project directory.
|
|
ans() { timeout 300 runuser -l greg -c "cd ~/ansible && $*" </dev/null; }
|
|
# play NAME PLAYBOOK [ARGS]: run one of greg's playbooks with a callback that
|
|
# records every result and the recap in $T/out/NAME.json; js NAME EXPR then
|
|
# tests a Python expression on them (see $T/q.py).
|
|
T=$(mktemp -d); chmod 755 "$T"; trap 'rm -rf "$T"' EXIT
|
|
install -d -o greg "$T/out"; mkdir "$T/cb"
|
|
cat >"$T/cb/le_lab_record.py" <<'X'
|
|
import json
|
|
import os
|
|
|
|
from ansible.plugins.callback import CallbackBase
|
|
|
|
|
|
class CallbackModule(CallbackBase):
|
|
CALLBACK_VERSION = 2.0
|
|
CALLBACK_TYPE = 'aggregate'
|
|
CALLBACK_NAME = 'le_lab_record'
|
|
CALLBACK_NEEDS_ENABLED = True
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.events = []
|
|
|
|
def _add(self, status, result):
|
|
t = result._task
|
|
r = {k: v for k, v in result._result.items() if k != 'ansible_facts'}
|
|
self.events.append({'host': result._host.get_name(), 'task': t.get_name(), 'action': t.action,
|
|
'role': t._role.get_name() if t._role else '', 'status': status, 'result': r})
|
|
|
|
def v2_runner_on_ok(self, result):
|
|
self._add('changed' if result._result.get('changed') else 'ok', result)
|
|
|
|
def v2_runner_on_failed(self, result, ignore_errors=False):
|
|
self._add('ignored' if ignore_errors else 'failed', result)
|
|
|
|
def v2_runner_on_skipped(self, result):
|
|
self._add('skipped', result)
|
|
|
|
def v2_runner_on_unreachable(self, result):
|
|
self._add('unreachable', result)
|
|
|
|
def v2_playbook_on_stats(self, stats):
|
|
out = {'events': self.events, 'stats': {h: stats.summarize(h) for h in stats.processed}}
|
|
with open(os.environ['LE_LAB_JSON'], 'w') as f:
|
|
json.dump(out, f, default=str)
|
|
X
|
|
cat >"$T/q.py" <<'X'
|
|
import json, re, sys
|
|
try:
|
|
d = json.load(open(sys.argv[1]))
|
|
except Exception:
|
|
sys.exit(1)
|
|
E, S = d['events'], d['stats']
|
|
def ev(host=None, status=None):
|
|
return [e for e in E if host in (None, e['host']) and status in (None, e['status'])]
|
|
def text(e):
|
|
return json.dumps({k: v for k, v in e['result'].items() if not k.startswith('_')})
|
|
def debug(e):
|
|
return e['action'].split('.')[-1] == 'debug'
|
|
def has(e, word):
|
|
return re.search(r'(?<![\w.])' + re.escape(str(word)) + r'(?![\w.])', text(e)) is not None
|
|
X
|
|
play() {
|
|
local o=$T/out/$1.json; shift
|
|
rm -f "$o"
|
|
ans "ANSIBLE_CALLBACK_PLUGINS=$T/cb ANSIBLE_CALLBACKS_ENABLED=le_lab_record LE_LAB_JSON=$o ansible-playbook $*" >"$T/out.log" 2>&1
|
|
}
|
|
js() { python3 -c "exec(open('$T/q.py').read()); sys.exit(0 if ($2) else 1)" "$T/out/$1.json"; }
|
|
cat >>"$T/q.py" <<'X'
|
|
def items(e):
|
|
return [r.get('item') for r in e['result'].get('results', [])]
|
|
def users_loop(e):
|
|
i = items(e)
|
|
return (len(i) >= 2 and all(isinstance(x, dict) for x in i)
|
|
and any('alice' in x.values() for x in i) and any('bob' in x.values() for x in i))
|
|
# What a looped result shows, whatever its key (msg, a var's name); with
|
|
# var: item, debug's key is the loop's own "item".
|
|
LOOP_KEYS = {'item', 'ansible_loop_var', 'ansible_index_var', 'changed', 'failed', 'skipped', 'skip_reason', 'false_condition'}
|
|
def shown(e, r):
|
|
s = {k: v for k, v in r.items() if not k.startswith('_') and k not in LOOP_KEYS}
|
|
return s if s or not debug(e) else {'item': r.get('item')}
|
|
def pairs_shown(e):
|
|
rs = e['result'].get('results', [])
|
|
return (len(rs) >= 2 and all(isinstance(r.get('item'), dict) and set(r['item']) == {'key', 'value'} for r in rs)
|
|
and all(has({'result': shown(e, r)}, r['item']['key']) and has({'result': shown(e, r)}, r['item']['value']) for r in rs))
|
|
def mem_split(h):
|
|
d = [e for e in ev(h) if debug(e)]
|
|
return (any(e['status'] == 'ok' for e in d)
|
|
and any(e['status'] == 'skipped' and 'mem' in str(e['result'].get('false_condition', '')) for e in d))
|
|
def failed_after(h):
|
|
# h failed a task, every other host ran a task after that, and only h failed.
|
|
i = next((n for n, e in enumerate(E) if e['host'] == h and e['status'] == 'failed'), None)
|
|
others = [o for o in S if o != h]
|
|
return (i is not None and others and S[h]['failures'] == 1
|
|
and all(S[o]['failures'] == 0 and S[o]['unreachable'] == 0 for o in others)
|
|
and all(any(e['host'] == o and e['status'] in ('ok', 'changed') for e in E[i + 1:]) for o in others))
|
|
X
|
|
P=/home/greg/ansible
|
|
|
|
task 1 "alice (in wheel) and bob on node1, from a list of dictionaries in one task"
|
|
need "alice is on node1, in wheel" 'in_node node1 id -nG alice | grep -qw wheel'
|
|
need "bob is on node1" 'in_node node1 id bob'
|
|
need "/home/greg/ansible/users.yml exists" "[ -f $P/users.yml ]"
|
|
need "ansible-playbook users.yml runs without errors" 'play users users.yml'
|
|
need "one task of users.yml loops over dictionaries for alice and bob on node1" 'js users "any(users_loop(e) for e in ev(\"node1\"))"'
|
|
need "running users.yml again changes nothing on node1 (both items ok)" 'js users "S[\"node1\"][\"changed\"] == 0"'
|
|
|
|
task 2 "memory.yml: one message on hosts with over 2 GiB, another on the rest"
|
|
need "/home/greg/ansible/memory.yml exists" "[ -f $P/memory.yml ]"
|
|
need "ansible-playbook memory.yml runs without errors" 'play memory memory.yml'
|
|
for n in $(nodes_names); do
|
|
need "on $n, one message shows and the other is skipped by a condition on memory" "js memory 'mem_split(\"$n\")'"
|
|
done
|
|
|
|
task 3 "services.yml: print each name and port of a dictionary, with dict2items"
|
|
need "/home/greg/ansible/services.yml exists" "[ -f $P/services.yml ]"
|
|
need "ansible-playbook services.yml runs without errors" 'play services services.yml'
|
|
need "a looped task prints each item.key and item.value of dict2items" 'js services "any(pairs_shown(e) for e in E)"'
|
|
need "services.yml uses dict2items" "grep -q dict2items $P/services.yml"
|
|
|
|
task 4 "fail.yml: a task fails on one host and the others carry on"
|
|
need "/home/greg/ansible/fail.yml exists" "[ -f $P/fail.yml ]"
|
|
need "ansible-playbook fail.yml reaches every node" 'play fail fail.yml; js fail "set(S) >= {\"node1\", \"node2\"}"'
|
|
need "the recap shows failed=1 for one host only, and the others ran tasks after it" 'js fail "any(failed_after(h) for h in S)"'
|