110 lines
5.1 KiB
Bash
110 lines
5.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 diffs(e):
|
|
x = e['result'].get('diff', [])
|
|
return x if isinstance(x, list) else [x]
|
|
def page(x):
|
|
return '/var/www/html/' in str(x.get('after_header', '')) + str(x.get('before_header', ''))
|
|
def page_diffs(e):
|
|
return [x for x in diffs(e) if page(x) and x.get('before') != x.get('after')]
|
|
X
|
|
# jp NAME EXPR: print a Python expression on the play's results.
|
|
jp() { python3 -c "exec(open('$T/q.py').read()); print($2)" "$T/out/$1.json" 2>/dev/null; }
|
|
# page: what node1 serves, retrying for up to 15 seconds.
|
|
page() {
|
|
local i
|
|
for i in $(seq 15); do curl -fsS --max-time 5 http://node1/ 2>/dev/null && return 0; sleep 1; done
|
|
return 1
|
|
}
|
|
P=/home/greg/ansible
|
|
# One run of site.yml in check mode, for tasks 1 and 2.
|
|
[ -f $P/site.yml ] && play site site.yml --check --diff
|
|
|
|
task 1 "site.yml: httpd with a home page on the webservers, mariadb-server on the dbservers"
|
|
need "/home/greg/ansible/site.yml exists" "[ -f $P/site.yml ]"
|
|
need "httpd is installed on node1" 'in_node node1 rpm -q httpd'
|
|
need "httpd is enabled on node1" 'in_node node1 systemctl is-enabled httpd'
|
|
need "httpd is running on node1" 'in_node node1 systemctl is-active httpd'
|
|
need "curl http://node1 from the control node prints a page" 'page | grep -q .'
|
|
need "mariadb-server is installed on node2" 'in_node node2 rpm -q mariadb-server'
|
|
need "site.yml --check runs on node1 and node2 with no failures" 'js site "ev(\"node1\") and ev(\"node2\") and not ev(status=\"failed\") and not ev(status=\"unreachable\")"'
|
|
need "another run of site.yml would change nothing but the page (--check)" 'js site "all(page_diffs(e) for e in ev(status=\"changed\"))"'
|
|
|
|
task 2 "A new page text, previewed on node1 but not made"
|
|
need "site.yml --check --diff would change the page on node1" 'js site "any(page_diffs(e) for e in ev(\"node1\", \"changed\"))"'
|
|
need "curl http://node1 still prints the old page" '[ "$(page)" = "$(jp site "next(x for e in ev(\"node1\", \"changed\") for x in page_diffs(e))[\"before\"].rstrip()")" ]'
|
|
|
|
task 3 "broken.yml: a task with bad indentation, the line in /root/lab-answers/3"
|
|
need "/home/greg/ansible/broken.yml exists" "[ -f $P/broken.yml ]"
|
|
need "ansible-playbook --syntax-check broken.yml fails, naming the file and a line" "! ans ansible-playbook broken.yml --syntax-check >$T/syntax.out 2>&1 && grep -Eq 'broken.yml.: line [0-9]+' $T/syntax.out"
|
|
need "/root/lab-answers/3 holds the line number it names" 'grep -qw "$(grep -Eo "broken.yml.: line [0-9]+" $T/syntax.out | grep -Eo "[0-9]+$" | head -n1)" /root/lab-answers/3'
|