108 lines
5.2 KiB
Bash
108 lines
5.2 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"; }
|
|
# hostvar HOST: HOST's http_port as the inventory sees it.
|
|
hostvar() { ans ansible-inventory --host "$1" 2>/dev/null | python3 -c 'import json, sys; print(json.load(sys.stdin).get("http_port", ""))'; }
|
|
# A host the lab adds to webservers for a moment, to see what the group gives.
|
|
printf '[webservers]\nlab-probe\n' >"$T/probe.ini"
|
|
probevar() { ans ansible-inventory -i inventory -i "$T/probe.ini" --host lab-probe 2>/dev/null | python3 -c 'import json, sys; print(json.load(sys.stdin).get("http_port", ""))'; }
|
|
# fact HOST NAME: a fact as the setup module reports it (JSON for a dictionary).
|
|
fact() { ans ansible "$1" -m ansible.builtin.setup -a "filter=ansible_$2" 2>/dev/null | sed '1s/^[^{]*//' | python3 -c 'import json, sys
|
|
v = json.load(sys.stdin)["ansible_facts"]["ansible_" + sys.argv[1]]
|
|
print(v if isinstance(v, (str, int)) else json.dumps(v))' "$2"; }
|
|
# shows NAME HOST VALUE: a debug task of the play printed VALUE on HOST.
|
|
shows() { [ -n "$3" ] && js "$1" "any(debug(e) and has(e, '$3') for e in ev('$2'))"; }
|
|
# facts_shown HOST: facts.yml printed HOST's distribution, version and memory.
|
|
facts_shown() {
|
|
local f
|
|
for f in distribution distribution_version memtotal_mb; do shows facts "$1" "$(fact "$1" $f)" || return 1; done
|
|
}
|
|
role_fact() { fact node1 local | python3 -c 'import json, sys; sys.exit(json.load(sys.stdin)["role"]["general"]["role"] != "web")'; }
|
|
|
|
task 1 "http_port: 80 for webservers, 8080 for node2, printed by port.yml"
|
|
need "webservers hosts get http_port 80 from the group" '[ "$(probevar)" = 80 ]'
|
|
need "node1's http_port is 80" '[ "$(hostvar node1)" = 80 ]'
|
|
need "node2's http_port is 8080" '[ "$(hostvar node2)" = 8080 ]'
|
|
need "/home/greg/ansible/port.yml exists" '[ -f /home/greg/ansible/port.yml ]'
|
|
need "ansible-playbook port.yml runs without errors" 'play port port.yml'
|
|
need "port.yml prints 80 on node1 and 8080 on node2" 'shows port node1 80 && shows port node2 8080'
|
|
need "with -e http_port=9000, port.yml prints 9000 on both" 'play port9 port.yml -e http_port=9000; shows port9 node1 9000 && shows port9 node2 9000'
|
|
|
|
task 2 "facts.yml prints each node's distribution, version and memory"
|
|
need "/home/greg/ansible/facts.yml exists" '[ -f /home/greg/ansible/facts.yml ]'
|
|
need "ansible-playbook facts.yml runs without errors" 'play facts facts.yml'
|
|
for n in $(nodes_names); do
|
|
need "facts.yml prints the distribution, its version and the memory on $n" "facts_shown $n"
|
|
done
|
|
|
|
task 3 "A custom fact role=web in [general] on node1"
|
|
need "node1 has /etc/ansible/facts.d/role.fact" 'in_node node1 test -f /etc/ansible/facts.d/role.fact'
|
|
need "the setup module shows role: web under general in ansible_local on node1" role_fact
|