EX294 practice labs #7
No files matched your search
@@ -0,0 +1,41 @@
|
||||
# EX294 1.1: Inventories, modules and ad hoc commands
|
||||
|
||||
Write an inventory with two groups, reach every host with the ping module,
|
||||
install a package with an ad hoc command, and pick hosts with a pattern.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab installs `ansible-core` and creates
|
||||
the user `greg` (password `Lab-Pass-1`), who has an SSH key. Work as greg
|
||||
(`su - greg` as root), in `/home/greg/ansible`: its `ansible.cfg` reads the
|
||||
inventory file `inventory`, which doesn't exist yet, and logs in as
|
||||
`ansible`. Answers go in `/root/lab-answers/<task>`, which the lab creates;
|
||||
write them as root.
|
||||
|
||||
The managed nodes `node1`, `node2` and `node3` (10.99.1.11 to .13) are
|
||||
AlmaLinux 10 containers with their own users and packages, reached over
|
||||
SSH by name. On each, the user `ansible` accepts greg's key and may use
|
||||
sudo without a password. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Write the inventory `/home/greg/ansible/inventory` with `node1` and
|
||||
`node2` in the group `webservers` and `node3` in `dbservers`, and show it
|
||||
as a tree.
|
||||
2. Run the ping module against every host; each must answer `pong`.
|
||||
3. Install the `tree` package on the webservers with an ad hoc command, then
|
||||
run the same command again: the first run says CHANGED, the second
|
||||
SUCCESS. `ansible webservers -a 'rpm -q tree'` finds it.
|
||||
4. List the hosts matched by `all:!dbservers` without running anything, and
|
||||
write the hosts it shows to `/root/lab-answers/4`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ansible-inventory -i inventory --graph`.
|
||||
- `ansible all -m ansible.builtin.ping`.
|
||||
- `ansible webservers -m ansible.builtin.dnf -a 'name=tree state=present'`
|
||||
needs `-b` to become root.
|
||||
- `--list-hosts`; quote the pattern so the shell leaves the `!` alone.
|
||||
Root can run Ansible from greg's project too:
|
||||
`cd /home/greg/ansible && ansible ... >/root/lab-answers/4`.
|
||||
@@ -0,0 +1,35 @@
|
||||
nodes_wait 60 || true
|
||||
# ans CMD: run CMD as greg in his project directory.
|
||||
ans() { timeout 180 runuser -l greg -c "cd ~/ansible && $*" </dev/null; }
|
||||
OUT=$LAB_STATE/check.out
|
||||
# members GROUP: the hosts in GROUP and its child groups, sorted, on one line.
|
||||
members() {
|
||||
ans ansible-inventory -i inventory --list 2>/dev/null | python3 -c 'import json, sys
|
||||
d = json.load(sys.stdin)
|
||||
def hosts(g):
|
||||
v = d.get(g, {})
|
||||
return set(v.get("hosts", [])).union(*[hosts(c) for c in v.get("children", [])])
|
||||
print(" ".join(sorted(hosts(sys.argv[1]))))' "$1"
|
||||
}
|
||||
under_all() { ans ansible-inventory -i inventory --list 2>/dev/null | python3 -c 'import json, sys
|
||||
sys.exit(0 if {"webservers", "dbservers"} <= set(json.load(sys.stdin)["all"].get("children", [])) else 1)'; }
|
||||
answer_has() { grep -qw "$2" "/root/lab-answers/$1" 2>/dev/null; }
|
||||
|
||||
task 1 "An inventory with node1 and node2 in webservers and node3 in dbservers"
|
||||
need "/home/greg/ansible/inventory exists" '[ -e /home/greg/ansible/inventory ]'
|
||||
need "webservers holds node1 and node2, and only those" '[ "$(members webservers)" = "node1 node2" ]'
|
||||
need "dbservers holds node3, and only it" '[ "$(members dbservers)" = node3 ]'
|
||||
need "both groups are directly under @all" under_all
|
||||
|
||||
task 2 "Every host answers the ping module"
|
||||
need "ansible all -m ping gets pong from node1, node2 and node3" 'ans ansible all -o -m ansible.builtin.ping >"$OUT"; for n in node1 node2 node3; do grep -Eq "^$n \| SUCCESS.*pong" "$OUT" || exit 1; done'
|
||||
|
||||
task 3 "tree on the webservers, and only there"
|
||||
need "tree is installed on node1" 'in_node node1 rpm -q tree'
|
||||
need "tree is installed on node2" 'in_node node2 rpm -q tree'
|
||||
need "tree is not installed on node3, which isn't a webserver" '! in_node node3 rpm -q tree'
|
||||
|
||||
task 4 "The hosts matched by all:!dbservers, in /root/lab-answers/4"
|
||||
need "/root/lab-answers/4 names node1 and node2" 'answer_has 4 node1 && answer_has 4 node2'
|
||||
need "/root/lab-answers/4 doesn't name node3" '! answer_has 4 node3'
|
||||
rm -f "$OUT"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.1 Inventories, modules and ad hoc commands"
|
||||
CHAPTER=/ex294/1-core-components/1-1-inventories-and-modules/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,5 @@
|
||||
# The nodes go with nodes_down, greg (with his project and key) with
|
||||
# users_remove, and /root/lab-answers with le-lab; ansible-core if setup
|
||||
# installed it.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,25 @@
|
||||
# The control node: ansible-core and greg's project, with no inventory yet
|
||||
# (task 1).
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'EOF'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = ansible
|
||||
host_key_checking = false
|
||||
EOF
|
||||
chown greg: /home/greg/ansible/ansible.cfg
|
||||
|
||||
# The managed nodes.
|
||||
nodes_up 3
|
||||
# greg's key goes to ansible on every node, and ansible may use sudo without
|
||||
# a password.
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'install -d -m 700 -o ansible -g ansible /home/ansible/.ssh &&
|
||||
install -m 600 -o ansible -g ansible /dev/stdin /home/ansible/.ssh/authorized_keys &&
|
||||
echo "ansible ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/ansible &&
|
||||
chmod 440 /etc/sudoers.d/ansible' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,44 @@
|
||||
# EX294 1.2: Variables and facts
|
||||
|
||||
Give a group and one host their own value of a variable, print facts from
|
||||
a playbook, and add a custom fact to a node.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab installs `ansible-core` and creates
|
||||
the user `greg` (password `Lab-Pass-1`), who has an SSH key. Work as greg
|
||||
(`su - greg` as root), in `/home/greg/ansible`: its `ansible.cfg` reads the
|
||||
inventory `inventory`, which has `node1` and `node2` in the group
|
||||
`webservers`, and logs in as `ansible`. There are no variable files yet.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers with their own users and packages, reached over
|
||||
SSH by name. On each, the user `ansible` accepts greg's key and may use
|
||||
sudo without a password. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Set `http_port` to 80 for the webservers group and to 8080 for node2
|
||||
alone, and print it from a playbook, `/home/greg/ansible/port.yml`, that
|
||||
runs on the webservers. node1 prints 80 and node2 8080; with
|
||||
`-e http_port=9000` both print 9000.
|
||||
2. Write `/home/greg/ansible/facts.yml`, which prints each managed node's
|
||||
distribution, its version and its memory in MiB from facts. Each host's
|
||||
numbers match `ansible HOST -m setup -a 'filter=ansible_memtotal_mb'`
|
||||
(and `ansible_distribution*`).
|
||||
3. Write a custom fact on node1 in `/etc/ansible/facts.d/role.fact`, with
|
||||
the section `[general]` and `role=web`.
|
||||
`ansible node1 -m setup -a 'filter=ansible_local'` shows `"role": "web"`
|
||||
under `general`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `group_vars/webservers.yml` and `host_vars/node2.yml` next to the
|
||||
inventory; `ansible-inventory --host node2` shows what a host gets.
|
||||
- `ansible.builtin.debug` with `msg: "{{ http_port }}"` or `var: http_port`.
|
||||
- `ansible_facts['distribution']`, `['distribution_version']` and
|
||||
`['memtotal_mb']`.
|
||||
- The fact file is plain INI and must not be executable (an executable
|
||||
`.fact` is run as a program). Write it with `ansible.builtin.copy` and
|
||||
`become: true`, or by hand on the node.
|
||||
@@ -0,0 +1,107 @@
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.2 Variables and facts"
|
||||
CHAPTER=/ex294/1-core-components/1-2-variables-and-facts/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,5 @@
|
||||
# The nodes go with nodes_down, greg (with his project and key) with
|
||||
# users_remove, and /root/lab-answers with le-lab; ansible-core if setup
|
||||
# installed it.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,29 @@
|
||||
# The control node: ansible-core and greg's project, with an inventory but
|
||||
# no variables (task 1).
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'EOF'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = ansible
|
||||
host_key_checking = false
|
||||
EOF
|
||||
printf '[webservers]
|
||||
node1
|
||||
node2
|
||||
' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes, with no custom facts (task 3).
|
||||
nodes_up 2
|
||||
# greg's key goes to ansible on every node, and ansible may use sudo without
|
||||
# a password.
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'install -d -m 700 -o ansible -g ansible /home/ansible/.ssh &&
|
||||
install -m 600 -o ansible -g ansible /dev/stdin /home/ansible/.ssh/authorized_keys &&
|
||||
echo "ansible ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/ansible &&
|
||||
chmod 440 /etc/sudoers.d/ansible' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,46 @@
|
||||
# EX294 1.3: Plays and playbooks
|
||||
|
||||
Write a playbook with two plays, preview a change with check mode, and
|
||||
find a YAML mistake with a syntax check.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab installs `ansible-core` and creates
|
||||
the user `greg` (password `Lab-Pass-1`), who has an SSH key. Work as greg
|
||||
(`su - greg` as root), in `/home/greg/ansible`: its `ansible.cfg` reads the
|
||||
inventory `inventory`, which has `node1` in the group `webservers` and
|
||||
`node2` in `dbservers`, and logs in as `ansible`. Answers go in
|
||||
`/root/lab-answers/<task>`, which the lab creates; write them as root.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers with their own users and packages, reached over
|
||||
SSH by name. On each, the user `ansible` accepts greg's key and may use
|
||||
sudo without a password. They run no firewall. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Write `/home/greg/ansible/site.yml` with one play that installs, starts
|
||||
and enables httpd and writes a home page,
|
||||
`/var/www/html/index.html`, on the webservers, and another play that
|
||||
installs `mariadb-server` on the dbservers. Run it twice: the second run
|
||||
shows `changed=0` for every host, and `curl http://node1` from the
|
||||
control node prints the page (open the firewall if one is running).
|
||||
2. Change the page's text in `site.yml` and preview the change on node1
|
||||
only, without making it: `--check --diff --limit node1` shows the old and
|
||||
the new line, and `curl http://node1` still prints the old text. Leave
|
||||
it that way.
|
||||
3. Copy `site.yml` to `/home/greg/ansible/broken.yml`, break the
|
||||
indentation of one task there, and find the error with `--syntax-check`.
|
||||
Write the line number it names to `/root/lab-answers/3`.
|
||||
|
||||
## Hints
|
||||
|
||||
- The plays are a YAML list: each starts with `- name:` and has `hosts:`,
|
||||
`become: true` and `tasks:`.
|
||||
- `ansible.builtin.dnf`, `ansible.builtin.copy` with `content:` and
|
||||
`dest:`, and `ansible.builtin.service` with `state: started` and
|
||||
`enabled: true`.
|
||||
- `ansible-playbook site.yml --check --diff --limit node1`.
|
||||
- `ansible-playbook broken.yml --syntax-check`; look for "line N" in the
|
||||
error.
|
||||
@@ -0,0 +1,109 @@
|
||||
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'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.3 Plays and playbooks"
|
||||
CHAPTER=/ex294/1-core-components/1-3-plays-and-playbooks/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,5 @@
|
||||
# The nodes go with nodes_down, greg (with his project and key) with
|
||||
# users_remove, and /root/lab-answers with le-lab; ansible-core if setup
|
||||
# installed it.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,30 @@
|
||||
# The control node: ansible-core and greg's project, with an inventory.
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'EOF'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = ansible
|
||||
host_key_checking = false
|
||||
EOF
|
||||
printf '[webservers]
|
||||
node1
|
||||
|
||||
[dbservers]
|
||||
node2
|
||||
' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes, without httpd or mariadb-server (task 1).
|
||||
nodes_up 2
|
||||
# greg's key goes to ansible on every node, and ansible may use sudo without
|
||||
# a password.
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'install -d -m 700 -o ansible -g ansible /home/ansible/.ssh &&
|
||||
install -m 600 -o ansible -g ansible /dev/stdin /home/ansible/.ssh/authorized_keys &&
|
||||
echo "ansible ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/ansible &&
|
||||
chmod 440 /etc/sudoers.d/ansible' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,48 @@
|
||||
# EX294 1.4: Loops, conditions and task failure
|
||||
|
||||
Create users from a list of dictionaries, run tasks only where a condition
|
||||
holds, loop over a dictionary, and watch one host fail while the others
|
||||
carry on.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab installs `ansible-core` and creates
|
||||
the user `greg` (password `Lab-Pass-1`), who has an SSH key. Work as greg
|
||||
(`su - greg` as root), in `/home/greg/ansible`: its `ansible.cfg` reads the
|
||||
inventory `inventory`, which has `node1` and `node2` in the group `nodes`,
|
||||
and logs in as `ansible`.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers with their own users and packages, reached over
|
||||
SSH by name. On each, the user `ansible` accepts greg's key and may use
|
||||
sudo without a password. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Write `/home/greg/ansible/users.yml`, which creates the users alice (in
|
||||
the group wheel) and bob on node1 from a list of dictionaries, in one
|
||||
task. `ansible node1 -a 'id alice'` shows wheel, and a second run shows
|
||||
both items `ok`.
|
||||
2. Write `/home/greg/ansible/memory.yml`, which prints one message only on
|
||||
hosts with more than 2 GiB of memory and another only on the rest. Each
|
||||
host shows one `ok` and one `skipping`.
|
||||
3. Write `/home/greg/ansible/services.yml`, which loops over a dictionary of
|
||||
service names and ports with `dict2items` and prints each pair: one line
|
||||
per key, showing `item.key` and `item.value`.
|
||||
4. Write `/home/greg/ansible/fail.yml`, in which a task fails on one host
|
||||
while the others carry on to the next task. The recap shows `failed=1`
|
||||
for that host only.
|
||||
|
||||
## Hints
|
||||
|
||||
- `loop: "{{ users }}"` over a list in `vars:`, with
|
||||
`name: "{{ item.name }}"` and `groups: "{{ item.groups }}"`.
|
||||
- `when: ansible_facts['memtotal_mb'] > 2048`, and `<= 2048` for the other
|
||||
task. All the hosts here have the same memory, so they all print the
|
||||
same message.
|
||||
- `loop: "{{ ports | dict2items }}"` with
|
||||
`msg: "{{ item.key }} {{ item.value }}"`.
|
||||
- `ansible.builtin.command: /bin/false` with
|
||||
`when: inventory_hostname == 'node1'`, then any task after it. Don't use
|
||||
`ignore_errors`.
|
||||
@@ -0,0 +1,126 @@
|
||||
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)"'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.4 Loops, conditions and task failure"
|
||||
CHAPTER=/ex294/1-core-components/1-4-loops-conditions-and-failures/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,5 @@
|
||||
# The nodes go with nodes_down, greg (with his project and key) with
|
||||
# users_remove, and /root/lab-answers with le-lab; ansible-core if setup
|
||||
# installed it.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,28 @@
|
||||
# The control node: ansible-core and greg's project, with an inventory.
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'EOF'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = ansible
|
||||
host_key_checking = false
|
||||
EOF
|
||||
printf '[nodes]
|
||||
node1
|
||||
node2
|
||||
' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes, without alice or bob (task 1).
|
||||
nodes_up 2
|
||||
# greg's key goes to ansible on every node, and ansible may use sudo without
|
||||
# a password.
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'install -d -m 700 -o ansible -g ansible /home/ansible/.ssh &&
|
||||
install -m 600 -o ansible -g ansible /dev/stdin /home/ansible/.ssh/authorized_keys &&
|
||||
echo "ansible ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/ansible &&
|
||||
chmod 440 /etc/sudoers.d/ansible' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,42 @@
|
||||
# EX294 1.5: Configuration files and roles
|
||||
|
||||
Find which `ansible.cfg` Ansible reads, list what it changes, and write a
|
||||
small role.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab installs `ansible-core` and creates
|
||||
the user `greg` (password `Lab-Pass-1`), who has an SSH key. Work as greg
|
||||
(`su - greg` as root), in `/home/greg/ansible`: its `ansible.cfg` reads the
|
||||
inventory `inventory`, which has `node1` and `node2` in the group `nodes`,
|
||||
and logs in as `ansible`. Answers go in `/root/lab-answers/<task>`, which
|
||||
the lab creates; write them as root.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers with their own users and packages, reached over
|
||||
SSH by name. On each, the user `ansible` accepts greg's key and may use
|
||||
sudo without a password. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. As greg, find which `ansible.cfg` Ansible uses in your project directory
|
||||
and in `/tmp`. Write the two paths to `/root/lab-answers/1`, the
|
||||
project directory's on line 1 and `/tmp`'s on line 2.
|
||||
2. List the settings your project's `ansible.cfg` changes, each with your
|
||||
file's path, and write the list to `/root/lab-answers/2`.
|
||||
3. Create a role `motd` in `/home/greg/ansible/roles/motd` that writes
|
||||
`/etc/motd` from a default variable, `motd_text`, and use it on every
|
||||
node with the playbook `/home/greg/ansible/motd.yml`.
|
||||
`ansible all -a 'cat /etc/motd'` prints the text, and running the
|
||||
playbook with `-e motd_text=Hello` changes it. Leave `/etc/motd` with
|
||||
the default text.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ansible --version | grep 'config file'`.
|
||||
- `ansible-config dump --only-changed`; root can run it from greg's
|
||||
project with `cd /home/greg/ansible && ... >/root/lab-answers/2`.
|
||||
- `ansible-galaxy role init roles/motd`; the task goes in `tasks/main.yml`
|
||||
and the variable in `defaults/main.yml`.
|
||||
- A play with `hosts: all`, `become: true` and `roles: [motd]`.
|
||||
@@ -0,0 +1,103 @@
|
||||
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"; }
|
||||
P=/home/greg/ansible
|
||||
# cfg_in DIR: the config file ansible --version names, run as greg in DIR.
|
||||
cfg_in() { timeout 120 runuser -l greg -c "cd $1 && ansible --version" </dev/null 2>/dev/null | sed -n 's/^ config file = //p'; }
|
||||
# line N: line N of /root/lab-answers/1, with ~ spelled out.
|
||||
line() { sed -n "${1}p" /root/lab-answers/1 2>/dev/null | sed 's/^[[:space:]]*//; s/[[:space:]]*$//; s|^~/|/home/greg/|'; }
|
||||
# changed: the settings greg's ansible.cfg changes, by ansible-config's names.
|
||||
changed() { ans ansible-config dump --only-changed 2>/dev/null | sed -n 's|^\([A-Z_0-9]*\)(/home/greg/ansible/ansible.cfg) = .*|\1|p'; }
|
||||
all_named() {
|
||||
local s n=0
|
||||
for s in $(changed); do grep -qw "$s" /root/lab-answers/2 || return 1; n=$((n + 1)); done
|
||||
[ "$n" -gt 0 ]
|
||||
}
|
||||
has_default() { python3 -c 'import sys, yaml; d = yaml.safe_load(open(sys.argv[1])); sys.exit(not (isinstance(d, dict) and "motd_text" in d))' $P/roles/motd/defaults/main.yml; }
|
||||
W=Hello-from-check-$RANDOM
|
||||
cat >>"$T/q.py" <<'X'
|
||||
def motd(h):
|
||||
return [e for e in ev(h) if e['role'] == 'motd']
|
||||
X
|
||||
|
||||
task 1 "The ansible.cfg in use in ~/ansible and in /tmp, in /root/lab-answers/1"
|
||||
need "line 1 of /root/lab-answers/1 is the file Ansible uses in /home/greg/ansible" '[ -n "$(line 1)" ] && [ "$(line 1)" = "$(cfg_in /home/greg/ansible)" ]'
|
||||
need "line 2 of /root/lab-answers/1 is the file Ansible uses in /tmp" '[ -n "$(line 2)" ] && [ "$(line 2)" = "$(cfg_in /tmp)" ]'
|
||||
|
||||
task 2 "The settings greg's ansible.cfg changes, in /root/lab-answers/2"
|
||||
need "/root/lab-answers/2 names every setting ansible-config dump --only-changed shows from greg's file" all_named
|
||||
|
||||
task 3 "A role motd writes /etc/motd from a default variable, on every node"
|
||||
need "/home/greg/ansible/roles/motd/tasks/main.yml exists" "[ -f $P/roles/motd/tasks/main.yml ]"
|
||||
need "the role's defaults/main.yml sets motd_text" has_default
|
||||
need "/home/greg/ansible/motd.yml exists" "[ -f $P/motd.yml ]"
|
||||
need "ansible-playbook motd.yml --check runs the role on every node, with nothing left to change" 'play motd motd.yml --check --diff; js motd "set(S) >= {\"node1\", \"node2\"} and all(motd(h) for h in S) and all(S[h][\"changed\"] == 0 and S[h][\"failures\"] == 0 for h in S)"'
|
||||
need "/etc/motd isn't empty on node1 and node2" 'in_node node1 test -s /etc/motd && in_node node2 test -s /etc/motd'
|
||||
need "with -e motd_text=TEXT, the role would write TEXT to /etc/motd on every node" 'play motdx motd.yml --check --diff -e motd_text=$W; js motdx "all(any(e[\"status\"] == \"changed\" and \"$W\" in text(e) for e in motd(h)) for h in S)"'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.5 Configuration files and roles"
|
||||
CHAPTER=/ex294/1-core-components/1-5-configuration-files-and-roles/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,5 @@
|
||||
# The nodes go with nodes_down, greg (with his project and key) with
|
||||
# users_remove, and /root/lab-answers with le-lab; ansible-core if setup
|
||||
# installed it.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,29 @@
|
||||
# The control node: ansible-core and greg's project, with an inventory.
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'EOF'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = ansible
|
||||
host_key_checking = false
|
||||
EOF
|
||||
printf '[nodes]
|
||||
node1
|
||||
node2
|
||||
' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes, with an empty /etc/motd (task 3).
|
||||
nodes_up 2
|
||||
# greg's key goes to ansible on every node, and ansible may use sudo without
|
||||
# a password.
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'install -d -m 700 -o ansible -g ansible /home/ansible/.ssh &&
|
||||
install -m 600 -o ansible -g ansible /dev/stdin /home/ansible/.ssh/authorized_keys &&
|
||||
echo "ansible ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/ansible &&
|
||||
chmod 440 /etc/sudoers.d/ansible' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
for n in $(nodes_names); do in_node "$n" ': >/etc/motd'; done
|
||||
@@ -0,0 +1,44 @@
|
||||
# EX294 1.6: Using the documentation
|
||||
|
||||
Find modules, options, examples and filters with `ansible-doc`, and turn
|
||||
an example from a module's page into a working task.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab installs `ansible-core` and creates
|
||||
the user `greg` (password `Lab-Pass-1`), who has an SSH key. Work as greg
|
||||
(`su - greg` as root), in `/home/greg/ansible`: its `ansible.cfg` reads the
|
||||
inventory `inventory`, which has `node1` in the group `nodes`, and logs in
|
||||
as `ansible`. Answers go in `/root/lab-answers/<task>`, which the
|
||||
lab creates; write them as root.
|
||||
|
||||
The managed node `node1` (10.99.1.11) is an AlmaLinux 10 container with its
|
||||
own users and packages, reached over SSH by name. The user `ansible`
|
||||
accepts greg's key and may use sudo without a password. For a root shell
|
||||
on it, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. As greg, install the `ansible.posix` collection from Ansible Galaxy.
|
||||
Then, using only `ansible-doc`, find the module that manages SELinux
|
||||
booleans and the option that makes the change persistent. Write the
|
||||
module's full name and the option's name to `/root/lab-answers/1`.
|
||||
2. Find an example in `ansible.builtin.lineinfile`'s page that changes a
|
||||
line matched by a regular expression, and adapt it in
|
||||
`/home/greg/ansible/sshd.yml` to set `PermitRootLogin no` in node1's
|
||||
`/etc/ssh/sshd_config`. The task runs, and a second run reports `ok`.
|
||||
3. List the filters available on the control node and find the one that
|
||||
hashes passwords. Write its name to `/root/lab-answers/3`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ansible-galaxy collection install ansible.posix` (it needs the
|
||||
internet); `ansible-doc -l | grep -i selinux`, then `ansible-doc MODULE`.
|
||||
For a module that isn't installed, `ansible-doc` only warns.
|
||||
- In the pager, `/EXAMPLES` jumps to the examples. The task needs
|
||||
`become: true`, and sshd reads the change only after a reload (a
|
||||
handler).
|
||||
- The node's sshd also reads `/etc/ssh/sshd_config.d/01-le-lab.conf`,
|
||||
which lets root log in for the lab and wins over the main file; leave it.
|
||||
`sshd -T | grep -i permitroot` shows what sshd really uses.
|
||||
- `ansible-doc -t filter -l | grep -i password`.
|
||||
@@ -0,0 +1,91 @@
|
||||
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"; }
|
||||
P=/home/greg/ansible
|
||||
F=/etc/ssh/sshd_config
|
||||
# The uncommented PermitRootLogin lines of node1's sshd_config.
|
||||
prl() { in_node node1 grep -Ei '^[[:space:]]*PermitRootLogin[[:space:]]' $F; }
|
||||
has() { grep -Fqi -- "$2" "/root/lab-answers/$1" 2>/dev/null; }
|
||||
|
||||
task 1 "ansible.posix installed; the module for SELinux booleans and its persistence option in /root/lab-answers/1"
|
||||
need "the ansible.posix collection is installed: ansible-doc finds ansible.posix.seboolean as greg" "timeout 120 runuser -l greg -c 'ansible-doc -l ansible.posix' </dev/null 2>/dev/null | grep -q '^ansible.posix.seboolean '"
|
||||
need "/root/lab-answers/1 names the module, by its full name" 'has 1 ansible.posix.seboolean'
|
||||
need "/root/lab-answers/1 names the option that makes the change persistent" 'grep -qw persistent /root/lab-answers/1'
|
||||
|
||||
task 2 "sshd.yml sets PermitRootLogin no with lineinfile, and a second run is ok"
|
||||
need "node1's $F has the line PermitRootLogin no" 'prl | grep -Eqi "^[[:space:]]*PermitRootLogin[[:space:]]+no[[:space:]]*$"'
|
||||
need "node1's $F has no other PermitRootLogin line" '[ "$(prl | wc -l)" = 1 ]'
|
||||
need "sshd accepts node1's configuration" 'in_node node1 sshd -t'
|
||||
need "/home/greg/ansible/sshd.yml exists" "[ -f $P/sshd.yml ]"
|
||||
need "ansible-playbook sshd.yml runs a lineinfile task on node1 without errors" 'play sshd sshd.yml; js sshd "any(e[\"action\"].split(\".\")[-1] == \"lineinfile\" for e in ev(\"node1\")) and S[\"node1\"][\"failures\"] == 0"'
|
||||
need "running sshd.yml again changes nothing (ok)" 'js sshd "S[\"node1\"][\"changed\"] == 0"'
|
||||
|
||||
task 3 "The filter that hashes passwords, in /root/lab-answers/3"
|
||||
need "/root/lab-answers/3 names the filter" 'has 3 password_hash'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.6 Using the documentation"
|
||||
CHAPTER=/ex294/1-core-components/1-6-documentation/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,9 @@
|
||||
# The nodes go with nodes_down, greg (with his project and key) with
|
||||
# users_remove, and /root/lab-answers with le-lab; ansible-core if setup
|
||||
# installed it.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
# The collection task 1 installs: greg's copy (it goes with greg too) and a
|
||||
# system-wide one setup didn't find.
|
||||
rm -rf /home/greg/.ansible/collections
|
||||
while read -r d; do rm -rf "$d"; done <"$LAB_STATE/new-dirs" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,32 @@
|
||||
# The control node: ansible-core and greg's project, with an inventory but
|
||||
# not the ansible.posix collection (task 1).
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'EOF'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = ansible
|
||||
host_key_checking = false
|
||||
EOF
|
||||
printf '[nodes]
|
||||
node1
|
||||
' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
# Task 1 installs ansible.posix; note whether there was a system-wide copy,
|
||||
# so reset can take away one the student adds there.
|
||||
[ -d /usr/share/ansible/collections/ansible_collections/ansible/posix ] ||
|
||||
echo /usr/share/ansible/collections/ansible_collections/ansible/posix >>"$LAB_STATE/new-dirs"
|
||||
|
||||
# The managed node; its sshd_config leaves PermitRootLogin commented out (task 2).
|
||||
nodes_up 1
|
||||
# greg's key goes to ansible on every node, and ansible may use sudo without
|
||||
# a password.
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'install -d -m 700 -o ansible -g ansible /home/ansible/.ssh &&
|
||||
install -m 600 -o ansible -g ansible /dev/stdin /home/ansible/.ssh/authorized_keys &&
|
||||
echo "ansible ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/ansible &&
|
||||
chmod 440 /etc/sudoers.d/ansible' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,52 @@
|
||||
# EX294 10.1: Templates
|
||||
|
||||
Generate files from Jinja2 templates: a hosts file built from every
|
||||
node's facts, a hardware report with each host's own values, and a
|
||||
configuration file checked with `validate` before it's installed.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) and installs `ansible-core`. Work as greg, in
|
||||
`/home/greg/ansible`: its `ansible.cfg` reads the inventory `inventory`,
|
||||
logs in to the nodes as `devops` with greg's SSH key, and becomes root.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are in
|
||||
the group `webservers`, `node3` (10.99.1.13) in `dbservers`; all three
|
||||
are AlmaLinux 10 containers. Containers share the VM's memory and disks,
|
||||
so the lab gives each node its own memory size and disks, as Ansible's
|
||||
facts see them: they differ from node to node. For a root shell on a
|
||||
node, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Generate `/etc/myhosts` on the database servers from a template that
|
||||
lists every managed node, one line each, as
|
||||
`ADDRESS FQDN SHORT-NAME`, for example
|
||||
`10.99.1.11 node1.lab.example node1`. The web servers get no
|
||||
`/etc/myhosts`.
|
||||
2. Write a hardware report template and deploy it to `/root/hwreport.txt`
|
||||
on every node, with each node's own values, in this form:
|
||||
|
||||
HOST=node1
|
||||
MEMORY=<memory in MiB>
|
||||
BIOS=<BIOS version>
|
||||
DISK_SIZE_VDB=<size of disk vdb>
|
||||
DISK_SIZE_VDD=<size of disk vdd>
|
||||
|
||||
A missing disk shows as `NONE`.
|
||||
3. Write `/home/greg/ansible/sshd.yml`, which templates the sshd drop-in
|
||||
`/etc/ssh/sshd_config.d/50-lab.conf` on all nodes with the line
|
||||
`PermitRootLogin {{ sshd_root_login }}` (the variable is `no` by
|
||||
default) and checks it with `validate` before installing it. Then try
|
||||
`-e sshd_root_login=maybe`: the task fails with "failed to validate",
|
||||
and the old file is unchanged.
|
||||
|
||||
## Hints
|
||||
|
||||
- `hostvars[host]['ansible_facts']` has another host's facts, but only
|
||||
if the play gathered them: target `all` and limit the task with `when`.
|
||||
- `ansible_facts['default_ipv4']['address']`, `['fqdn']`, `['hostname']`.
|
||||
- `ansible_facts['memtotal_mb']`, `['bios_version']`, and
|
||||
`ansible_facts['devices']['vdb']['size'] | default('NONE')`.
|
||||
- `validate: /usr/sbin/sshd -t -f %s`.
|
||||
@@ -0,0 +1,42 @@
|
||||
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; }
|
||||
OUT=$LAB_STATE/check.out
|
||||
D=/etc/ssh/sshd_config.d/50-lab.conf
|
||||
# What each node should report: memory in MiB, vdb and vdd.
|
||||
declare -A MEM=([node1]=4096 [node2]=2048 [node3]=1024)
|
||||
declare -A VDB=([node1]="2.00 GB" [node2]="2.00 GB" [node3]=NONE)
|
||||
declare -A VDD=([node1]="1.00 GB" [node2]=NONE [node3]=NONE)
|
||||
# The BIOS version as Ansible sees it (the VM's, on every node).
|
||||
BIOS=$(ans "ansible node1 -m setup -a filter=ansible_bios_version" 2>/dev/null | sed -n 's/^ *"ansible_bios_version": "\(.*\)",\{0,1\}$/\1/p')
|
||||
# line NODE FILE TEXT: FILE on NODE has the line TEXT (spaces at the end aside).
|
||||
line() { in_node "$1" cat "$2" | sed 's/[[:space:]]*$//' | grep -qxF "$3"; }
|
||||
|
||||
task 1 "/etc/myhosts on the database servers: every node's address, FQDN and name"
|
||||
need "node3 has /etc/myhosts" 'in_node node3 test -f /etc/myhosts'
|
||||
for n in node1 node2 node3; do
|
||||
need "node3's /etc/myhosts has \"$(node_addr $n) $n.lab.example $n\"" "in_node node3 cat /etc/myhosts | awk -v a=$(node_addr $n) -v n=$n '\$1 == a && \$2 == n \".lab.example\" && \$3 == n && NF == 3 { ok = 1 } END { exit !ok }'"
|
||||
done
|
||||
need "it has one line per node" '[ "$(in_node node3 grep -c "^10\.99\.1\." /etc/myhosts)" = 3 ]'
|
||||
need "node1 and node2, the web servers, have no /etc/myhosts" '! in_node node1 test -e /etc/myhosts && ! in_node node2 test -e /etc/myhosts'
|
||||
|
||||
task 2 "/root/hwreport.txt on every node, with NONE for a missing disk"
|
||||
need "Ansible reports the BIOS version" '[ -n "$BIOS" ]'
|
||||
for n in node1 node2 node3; do
|
||||
need "$n has /root/hwreport.txt" "in_node $n test -f /root/hwreport.txt"
|
||||
need "$n's report says HOST=$n" "line $n /root/hwreport.txt HOST=$n"
|
||||
need "$n's report says MEMORY=${MEM[$n]}" "line $n /root/hwreport.txt MEMORY=${MEM[$n]}"
|
||||
need "$n's report says BIOS=$BIOS" 'line '$n' /root/hwreport.txt "BIOS=$BIOS"'
|
||||
need "$n's report says DISK_SIZE_VDB=${VDB[$n]}" "line $n /root/hwreport.txt 'DISK_SIZE_VDB=${VDB[$n]}'"
|
||||
need "$n's report says DISK_SIZE_VDD=${VDD[$n]}" "line $n /root/hwreport.txt 'DISK_SIZE_VDD=${VDD[$n]}'"
|
||||
done
|
||||
|
||||
task 3 "sshd.yml: a validated sshd drop-in that keeps the old file on a bad value"
|
||||
need "/home/greg/ansible/sshd.yml exists" '[ -f /home/greg/ansible/sshd.yml ]'
|
||||
need "ansible-playbook sshd.yml runs without failures" 'ans ansible-playbook sshd.yml >"$OUT"; grep -Eq "^node[0-9]+ +: ok=" "$OUT" && ! grep -E " : ok=" "$OUT" | grep -Eq "(unreachable|failed)=[1-9]"'
|
||||
for n in node1 node2 node3; do
|
||||
need "$n's $D says PermitRootLogin no" "line $n $D 'PermitRootLogin no'"
|
||||
done
|
||||
need "with -e sshd_root_login=maybe, the task fails with \"failed to validate\"" 'in_node node1 md5sum $D >"$OUT.sum"; ans ansible-playbook sshd.yml -e sshd_root_login=maybe >"$OUT"; grep -q "failed to validate" "$OUT" && grep -Eq "^node1 +: .*failed=1" "$OUT"'
|
||||
need "and node1's $D is unchanged" '[ "$(in_node node1 md5sum $D)" = "$(cat "$OUT.sum")" ]'
|
||||
rm -f "$OUT" "$OUT.sum"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="10.1 Templates"
|
||||
CHAPTER=/ex294/10-managing-content/10-1-templates/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,9 @@
|
||||
# The nodes go with nodes_down and greg (with his project) with
|
||||
# users_remove; here the hardware unit and what setup installed.
|
||||
if [ -f /etc/systemd/system/le-lab-node-hw.service ]; then
|
||||
systemctl disable --quiet --now le-lab-node-hw.service 2>/dev/null
|
||||
rm -f /etc/systemd/system/le-lab-node-hw.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,80 @@
|
||||
# The control node: ansible-core, and greg with an SSH key and his project.
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'X'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
X
|
||||
printf '[webservers]\nnode1\nnode2\n\n[dbservers]\nnode3\n' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes, each with devops: greg's key and passwordless sudo.
|
||||
nodes_up 3
|
||||
for n in $(nodes_names); do
|
||||
in_node $n 'useradd -m devops && echo "devops ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/devops && chmod 440 /etc/sudoers.d/devops && install -d -m 700 -o devops -g devops /home/devops/.ssh'
|
||||
in_node -i $n 'install -m 600 -o devops -g devops /dev/stdin /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
|
||||
# Nodes share the VM's memory and disks. So that facts differ by host, each
|
||||
# node gets its own /proc/meminfo and /sys/block (bind mounts made from the
|
||||
# VM into the node), again at every boot by le-lab-node-hw.service.
|
||||
# NAME MEMORY-MiB DISK:GiB...
|
||||
cat >"$LAB_STATE/hardware" <<'X'
|
||||
node1 4096 vda:20 vdb:2 vdd:1
|
||||
node2 2048 vda:20 vdb:2
|
||||
node3 1024 vda:20
|
||||
X
|
||||
cat >"$LAB_STATE/hardware.sh" <<'X'
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
while read -r name mem disks; do
|
||||
pid=$(podman inspect -f '{{.State.Pid}}' "$name" 2>/dev/null)
|
||||
[ "${pid:-0}" -gt 0 ] || continue
|
||||
for m in /proc/meminfo /sys/block; do
|
||||
nsenter -t "$pid" -m findmnt -n --mountpoint "$m" >/dev/null && nsenter -t "$pid" -m umount "$m"
|
||||
done
|
||||
podman exec -i "$name" bash -s -- "$mem" $disks <<'Y'
|
||||
d=/var/lib/le-lab-hw mem=$1; shift
|
||||
rm -rf $d; mkdir -p $d/block
|
||||
awk -v kb=$((mem * 1024)) '
|
||||
/^MemTotal:/ { printf "MemTotal: %8d kB\n", kb; next }
|
||||
/^(MemFree|MemAvailable):/ { printf "%-16s%8d kB\n", $1, kb / 2; next }
|
||||
{ print }' /proc/meminfo >$d/meminfo
|
||||
for x; do
|
||||
b=$d/block/${x%%:*}
|
||||
mkdir -p $b/device $b/queue
|
||||
echo $((${x#*:} * 2097152)) >$b/size
|
||||
echo 0 >$b/removable; echo 1 >$b/queue/rotational
|
||||
echo 512 >$b/queue/logical_block_size; echo 512 >$b/queue/physical_block_size
|
||||
echo "Virtual disk" >$b/device/model
|
||||
done
|
||||
Y
|
||||
nsenter -t "$pid" -m mount --bind /var/lib/le-lab-hw/meminfo /proc/meminfo
|
||||
nsenter -t "$pid" -m mount --bind /var/lib/le-lab-hw/block /sys/block
|
||||
done <hardware
|
||||
X
|
||||
cat >/etc/systemd/system/le-lab-node-hw.service <<X
|
||||
[Unit]
|
||||
Description=LINUXexpert practice lab: each node's memory and disks
|
||||
After=le-lab-nodes.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/bin/bash $LAB_STATE/hardware.sh
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
X
|
||||
systemctl daemon-reload
|
||||
systemctl enable --quiet --now le-lab-node-hw.service
|
||||
|
||||
# Ready to go: greg reaches every node as root.
|
||||
timeout 120 runuser -l greg -c 'cd ~/ansible && ansible all -o -a id' </dev/null | grep -c 'uid=0(root)' | grep -qx 3
|
||||
@@ -0,0 +1,45 @@
|
||||
# EX294 10.2: Ansible Vault
|
||||
|
||||
Keep a user's password in an encrypted variables file, use it to create
|
||||
the user, change the vault password, and run the playbook with
|
||||
ansible-navigator, with a password file and at the prompt.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core` and
|
||||
`ansible-navigator`. Work as greg, in `/home/greg/ansible`: its
|
||||
`ansible.cfg` reads the inventory `inventory`, logs in as `devops` and
|
||||
becomes root. Its `ansible-navigator.yml` turns execution environments
|
||||
off, so ansible-navigator runs this VM's own `ansible-playbook` and pulls
|
||||
no container image.
|
||||
|
||||
The managed node `node1` (10.99.1.11) is an AlmaLinux 10 container
|
||||
reached over SSH by name, where `devops` has greg's key and passwordless
|
||||
sudo. For a root shell on it, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. In `/home/greg/ansible`, store the vault password `RedhatVault` in
|
||||
`vault-pass.txt` (mode 600), and create the encrypted variables file
|
||||
`secret.yml` with `user_password` set to a password of your choosing.
|
||||
Write `vault.yml`, which uses `secret.yml` to create the user
|
||||
`vaultuser` on node1 with that password, and run it.
|
||||
2. Change the vault password of `secret.yml`, and put the new one in
|
||||
`vault-pass.txt`. Then `ansible-vault view` works with the new
|
||||
password and fails with `RedhatVault`.
|
||||
3. Run `vault.yml` with ansible-navigator, first with
|
||||
`--vault-password-file vault-pass.txt`, then with `--ask-vault-pass`.
|
||||
Set `ansible-navigator.yml` up so the prompt works: stdout mode, and
|
||||
playbook artifacts off.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ansible-vault create`, `encrypt`, `view`, `rekey`
|
||||
(`--new-vault-password-file`).
|
||||
- `vars_files: [secret.yml]`, and
|
||||
`password: "{{ user_password | password_hash('sha512', 'vaultsalt') }}"`
|
||||
with `update_password: on_create`.
|
||||
- In `ansible-navigator.yml`: `mode: stdout` and `playbook-artifact:` with
|
||||
`enable: false`, beside the `execution-environment` setting. With
|
||||
artifacts on, `--ask-vault-pass` shows no prompt and waits.
|
||||
@@ -0,0 +1,75 @@
|
||||
nodes_wait 60 || true
|
||||
# ans CMD: run CMD as greg in his project directory.
|
||||
ans() { setsid -w timeout -k 5 180 runuser -l greg -c "cd ~/ansible && $*" </dev/null; }
|
||||
A=/home/greg/ansible
|
||||
OUT=$LAB_STATE/check.out
|
||||
# view [PASSWORD]: secret.yml decrypted with vault-pass.txt, or with PASSWORD.
|
||||
view() {
|
||||
if [ $# -eq 0 ]; then ans ansible-vault view secret.yml --vault-password-file vault-pass.txt
|
||||
else ans "ansible-vault view secret.yml --vault-password-file <(echo '$1')"; fi
|
||||
}
|
||||
# The vault's user_password, if the password file opens it.
|
||||
PW=$(view 2>/dev/null | python3 -c 'import sys, yaml
|
||||
d = yaml.safe_load(sys.stdin)
|
||||
print(d.get("user_password", "") if isinstance(d, dict) else "")' 2>/dev/null)
|
||||
# pw_ok: vaultuser's password on node1 is $PW.
|
||||
pw_ok() {
|
||||
local h; h=$(in_node node1 getent shadow vaultuser | cut -d: -f2)
|
||||
[ -n "$PW" ] && [ -n "$h" ] && PW=$PW H=$h python3 -W ignore -c 'import crypt, os, sys
|
||||
sys.exit(crypt.crypt(os.environ["PW"], os.environ["H"]) != os.environ["H"])'
|
||||
}
|
||||
# A terminal for --ask-vault-pass: askpass FILE CMD... answers the prompt
|
||||
# once with the password in FILE and exits with CMD's status (99 if no
|
||||
# prompt came).
|
||||
cat >"$LAB_STATE/askpass.py" <<'P'
|
||||
import os, pty, select, sys, time
|
||||
pw, cmd = open(sys.argv[1]).read().strip(), sys.argv[2:]
|
||||
pid, fd = pty.fork()
|
||||
if pid == 0:
|
||||
os.execvp(cmd[0], cmd)
|
||||
buf, sent, end = b"", False, time.time() + 60
|
||||
while time.time() < end:
|
||||
if not select.select([fd], [], [], 1)[0]:
|
||||
continue
|
||||
try:
|
||||
d = os.read(fd, 4096)
|
||||
except OSError:
|
||||
break
|
||||
if not d:
|
||||
break
|
||||
buf += d
|
||||
if not sent and b"assword:" in buf:
|
||||
os.write(fd, pw.encode() + b"\n")
|
||||
sent = True
|
||||
sys.stdout.write(buf.decode(errors="replace"))
|
||||
if time.time() >= end:
|
||||
os.kill(pid, 9)
|
||||
_, st = os.waitpid(pid, 0)
|
||||
sys.exit(os.waitstatus_to_exitcode(st) if sent else 99)
|
||||
P
|
||||
chmod 644 "$LAB_STATE/askpass.py"
|
||||
navcfg() {
|
||||
python3 -c 'import sys, yaml
|
||||
n = (yaml.safe_load(open(sys.argv[1])) or {}).get("ansible-navigator", {})
|
||||
ok = n.get("mode") == "stdout" and (n.get("playbook-artifact") or {}).get("enable") in (False, "false")
|
||||
sys.exit(0 if ok else 1)' $A/ansible-navigator.yml
|
||||
}
|
||||
|
||||
task 1 "An encrypted secret.yml, and vaultuser on node1 with its password"
|
||||
need "$A/vault-pass.txt exists with mode 600" "[ \"\$(stat -c %a $A/vault-pass.txt)\" = 600 ]"
|
||||
need "$A/secret.yml is encrypted (starts with \$ANSIBLE_VAULT)" "head -1 $A/secret.yml | grep -q '^\\\$ANSIBLE_VAULT'"
|
||||
need "vault-pass.txt opens secret.yml, which sets user_password" '[ -n "$PW" ]'
|
||||
need "$A/vault.yml exists and uses secret.yml" "grep -q secret.yml $A/vault.yml"
|
||||
need "vaultuser exists on node1" 'in_node node1 id vaultuser'
|
||||
need "vaultuser's password on node1 is user_password from the vault" pw_ok
|
||||
|
||||
task 2 "A new vault password, in vault-pass.txt"
|
||||
need "vault-pass.txt opens secret.yml" '[ -n "$PW" ]'
|
||||
need "vault-pass.txt doesn't hold RedhatVault any more" "! grep -qx RedhatVault $A/vault-pass.txt"
|
||||
need "RedhatVault no longer opens secret.yml" '! view RedhatVault'
|
||||
|
||||
task 3 "vault.yml runs in ansible-navigator, with the password file and the prompt"
|
||||
need "ansible-navigator.yml sets mode: stdout and playbook artifacts off" navcfg
|
||||
need "ansible-navigator run vault.yml --vault-password-file vault-pass.txt works" 'ans ansible-navigator run vault.yml --vault-password-file vault-pass.txt >"$OUT" && grep -q "PLAY RECAP" "$OUT"'
|
||||
need "ansible-navigator run vault.yml --ask-vault-pass asks and works" "[ -n \"\$PW\" ] && ans python3 -I $LAB_STATE/askpass.py vault-pass.txt ansible-navigator run vault.yml --ask-vault-pass >\"\$OUT\" && grep -q 'PLAY RECAP' \"\$OUT\""
|
||||
rm -f "$OUT"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="10.2 Ansible Vault"
|
||||
CHAPTER=/ex294/10-managing-content/10-2-ansible-vault/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,8 @@
|
||||
# The node goes with nodes_down and greg (with his project) with
|
||||
# users_remove; ansible-navigator and the packages the lab installed here.
|
||||
if [ "$(state_get navigator)" = /opt/le-lab-navigator ]; then
|
||||
rm -f /usr/local/bin/ansible-navigator
|
||||
rm -rf /opt/le-lab-navigator
|
||||
fi
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,45 @@
|
||||
# The control node: ansible-core, ansible-navigator (in its own Python
|
||||
# environment) and greg's project, with his key.
|
||||
for p in ansible-core; do pkg_installed $p || echo $p >>"$LAB_STATE/missing"; done
|
||||
pkg_installed ansible-core || pkg_install ansible-core
|
||||
if ! command -v ansible-navigator >/dev/null; then
|
||||
say "${C_DIM}Installing ansible-navigator...${C_0}"
|
||||
state_set navigator /opt/le-lab-navigator
|
||||
python3 -m venv /opt/le-lab-navigator
|
||||
/opt/le-lab-navigator/bin/pip install -q --disable-pip-version-check ansible-navigator >/dev/null ||
|
||||
die "could not install ansible-navigator from PyPI"
|
||||
ln -sf /opt/le-lab-navigator/bin/ansible-navigator /usr/local/bin/ansible-navigator
|
||||
fi
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'C'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
C
|
||||
printf '[nodes]\nnode1\n' >/home/greg/ansible/inventory
|
||||
cat >/home/greg/ansible/ansible-navigator.yml <<'C'
|
||||
---
|
||||
ansible-navigator:
|
||||
execution-environment:
|
||||
enabled: false
|
||||
C
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory /home/greg/ansible/ansible-navigator.yml
|
||||
|
||||
# The managed node: devops with greg's key and sudo, and no vaultuser.
|
||||
nodes_up 1
|
||||
in_node -i node1 'id devops >/dev/null 2>&1 || useradd -m devops
|
||||
echo devops:Lab-Pass-1 | chpasswd
|
||||
echo "devops ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/devops
|
||||
chmod 440 /etc/sudoers.d/devops
|
||||
install -d -m 700 -o devops -g devops /home/devops/.ssh
|
||||
cat >/home/devops/.ssh/authorized_keys
|
||||
chown devops: /home/devops/.ssh/authorized_keys
|
||||
chmod 600 /home/devops/.ssh/authorized_keys
|
||||
userdel -r vaultuser 2>/dev/null; true' </home/greg/.ssh/id_ed25519.pub
|
||||
@@ -0,0 +1,42 @@
|
||||
# EX294 2.1: ansible.cfg
|
||||
|
||||
Write a project's `ansible.cfg`, check what Ansible makes of it, and find
|
||||
a setting that Ansible ignores.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core`. Work as greg.
|
||||
His project `/home/greg/ansible` has an inventory, `inventory`, listing the
|
||||
managed nodes, but no `ansible.cfg` yet. A second project,
|
||||
`/home/greg/proj2`, has an `ansible.cfg` with a mistake in it.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name. On both, the automation
|
||||
user `devops` accepts greg's key and may use sudo without a password. For
|
||||
a root shell on a node, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. In `/home/greg/ansible`, write an `ansible.cfg` that uses `./inventory`,
|
||||
logs in as `devops`, puts roles in `./roles` and collections in
|
||||
`./collections`, and becomes root with sudo without asking for a
|
||||
password. `ansible-config dump --only-changed` shows each setting with
|
||||
your file's path, and `ansible all -a whoami` prints `root` for every
|
||||
node.
|
||||
2. Set `forks` to 10 in the same file, so `ansible-config dump | grep
|
||||
FORKS` shows 10.
|
||||
3. One setting in `/home/greg/proj2/ansible.cfg` is misspelled, so Ansible
|
||||
ignores it. Find it with `ansible-config dump --only-changed` (run in
|
||||
`/home/greg/proj2`) and correct it, so every setting in that file
|
||||
takes effect.
|
||||
|
||||
## Hints
|
||||
|
||||
- `[defaults]`: `inventory`, `remote_user`, `roles_path`,
|
||||
`collections_path`, `forks`, `host_key_checking`.
|
||||
- `[privilege_escalation]`: `become`, `become_method`, `become_user`,
|
||||
`become_ask_pass`.
|
||||
- `ansible --version` names the config file in use; Ansible only reads
|
||||
`./ansible.cfg` from the directory you run it in.
|
||||
- `ansible-config init --disabled` lists every setting's correct name.
|
||||
@@ -0,0 +1,30 @@
|
||||
nodes_wait 60 || true
|
||||
# ans DIR CMD: run CMD as greg in /home/greg/DIR.
|
||||
ans() { local d=$1; shift; timeout 120 runuser -l greg -c "cd ~/$d && $*" </dev/null; }
|
||||
P=/home/greg/ansible C=/home/greg/ansible/ansible.cfg
|
||||
D=$(ans ansible ansible-config dump --only-changed 2>/dev/null)
|
||||
OUT=$LAB_STATE/check.out Q="'"
|
||||
|
||||
task 1 "ansible.cfg: inventory, devops, roles, collections, sudo without a password"
|
||||
need "$C exists" '[ -f "$C" ]'
|
||||
need "ansible --version in $P names $C" 'ans ansible ansible --version | grep -qx " config file = $C"'
|
||||
need "the dump shows inventory $P/inventory from $C" 'grep -Fqx "DEFAULT_HOST_LIST($C) = [$Q$P/inventory$Q]" <<<"$D"'
|
||||
need "the dump shows remote_user devops from $C" 'grep -Fqx "DEFAULT_REMOTE_USER($C) = devops" <<<"$D"'
|
||||
need "the dump shows roles_path with $P/roles from $C" 'grep -F "DEFAULT_ROLES_PATH($C) = " <<<"$D" | grep -Fq "$Q$P/roles$Q"'
|
||||
need "the dump shows collections_path with $P/collections from $C" 'grep -F "COLLECTIONS_PATHS($C) = " <<<"$D" | grep -Fq "$Q$P/collections$Q"'
|
||||
need "the dump shows become True from $C" 'grep -Fqx "DEFAULT_BECOME($C) = True" <<<"$D"'
|
||||
need "the become method is sudo" 'ans ansible ansible-config dump | grep -Eq "^DEFAULT_BECOME_METHOD\(.*\) = sudo$"'
|
||||
need "the become user is root" 'ans ansible ansible-config dump | grep -Eq "^DEFAULT_BECOME_USER\(.*\) = root$"'
|
||||
need "Ansible doesn't ask for the sudo password" '! grep -Eq "^DEFAULT_BECOME_ASK_PASS\(.*\) = True$" <<<"$D"'
|
||||
need "ansible all -a whoami prints root for node1 and node2" 'ans ansible ansible all -o -a whoami >"$OUT"; for n in node1 node2; do grep -Eq "^$n .*\(stdout\) root$" "$OUT" || exit 1; done'
|
||||
|
||||
task 2 "forks = 10"
|
||||
need "ansible-config dump in $P shows DEFAULT_FORKS = 10 from $C" 'ans ansible ansible-config dump | grep -Fqx "DEFAULT_FORKS($C) = 10"'
|
||||
|
||||
task 3 "Every setting in proj2's ansible.cfg takes effect"
|
||||
C2=/home/greg/proj2/ansible.cfg
|
||||
D2=$(ans proj2 ansible-config dump --only-changed 2>/dev/null)
|
||||
need "the dump in proj2 shows remote_user devops from $C2" 'grep -Fqx "DEFAULT_REMOTE_USER($C2) = devops" <<<"$D2"'
|
||||
need "the dump in proj2 still shows inventory, host_key_checking and forks 20 from $C2" 'grep -Fq "DEFAULT_HOST_LIST($C2) = " <<<"$D2" && grep -Fqx "HOST_KEY_CHECKING($C2) = False" <<<"$D2" && grep -Fqx "DEFAULT_FORKS($C2) = 20" <<<"$D2"'
|
||||
need "$C2 has no misspelled remote_usr line left" '! grep -Eq "^[[:space:]]*remote_usr[[:space:]]*=" "$C2"'
|
||||
rm -f "$OUT"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.1 ansible.cfg"
|
||||
CHAPTER=/ex294/2-configuring-ansible/2-1-ansible-cfg/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,4 @@
|
||||
# The nodes go with nodes_down and greg (with both projects) with
|
||||
# users_remove; ansible-core goes if the lab installed it.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,25 @@
|
||||
# The control node: ansible-core, greg with a key, his project without an
|
||||
# ansible.cfg (tasks 1 and 2), and proj2 with a misspelled setting (task 3).
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
for d in ansible proj2; do
|
||||
install -d -m 755 -o greg -g greg /home/greg/$d
|
||||
printf '[nodes]\nnode1\nnode2\n' >/home/greg/$d/inventory
|
||||
done
|
||||
cat >/home/greg/proj2/ansible.cfg <<'X'
|
||||
[defaults]
|
||||
inventory = ./inventory
|
||||
remote_usr = devops
|
||||
host_key_checking = false
|
||||
forks = 20
|
||||
X
|
||||
chown -R greg: /home/greg/ansible /home/greg/proj2
|
||||
|
||||
# The managed nodes, with devops: greg's key and passwordless sudo.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node "$n" 'useradd -m devops && printf "devops ALL=(ALL) NOPASSWD: ALL\n" >/etc/sudoers.d/devops && chmod 440 /etc/sudoers.d/devops && install -d -m 700 -o devops -g devops /home/devops/.ssh'
|
||||
in_node -i "$n" 'cat >/home/devops/.ssh/authorized_keys && chown devops: /home/devops/.ssh/authorized_keys && chmod 600 /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,51 @@
|
||||
# EX294 2.2: ansible-navigator.yml
|
||||
|
||||
Configure ansible-navigator for a project: a local execution environment
|
||||
image, the pull policy, stdout mode and no artifacts. Then see what a bad
|
||||
pull policy and a run without the execution environment do.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core` and
|
||||
`ansible-navigator` (from PyPI, in `/opt/le-lab-navigator`). Work as
|
||||
greg, in `/home/greg/ansible`: its `ansible.cfg` reads the inventory
|
||||
`inventory`, logs in as `devops` and becomes root, and `site.yml` is a
|
||||
playbook for every node.
|
||||
|
||||
greg's podman has one execution environment image, `localhost/lab-ee:2.16`,
|
||||
built here from public parts, the way the chapter's "Getting navigator
|
||||
and an image" shows: AlmaLinux 10's ansible-core with the `ansible.posix`
|
||||
and `community.general` collections from Ansible Galaxy.
|
||||
It exists only on this VM, in no registry, and needs no login.
|
||||
`podman images` (as greg) lists it.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name; on both, `devops`
|
||||
accepts greg's key and may use sudo without a password. For a root shell
|
||||
on a node, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Write `/home/greg/ansible/ansible-navigator.yml` so that navigator uses
|
||||
the image `localhost/lab-ee:2.16`, never pulls, runs in stdout mode and
|
||||
writes no playbook artifacts. `ansible-navigator settings --effective
|
||||
--sources` shows `Settings file` for each of them, and
|
||||
`ansible-navigator run site.yml` prints plain playbook output.
|
||||
2. Run `site.yml` once with the pull policy `always` (`--pp always`) and
|
||||
read the error. Write the line navigator stops with to
|
||||
`/root/lab-answers/2` (as root).
|
||||
3. Run `site.yml` with the execution environment turned off
|
||||
(`--ee false`). Which module fails, and why? Write its full name to
|
||||
`/root/lab-answers/3` (as root).
|
||||
|
||||
## Hints
|
||||
|
||||
- Everything sits under the top-level `ansible-navigator:` key, and
|
||||
setting names use hyphens: `execution-environment` (`image`,
|
||||
`pull: policy:`), `mode`, `playbook-artifact` (`enable`).
|
||||
- `ansible-navigator settings --sample` prints a commented example of
|
||||
every setting. A key that shows `Defaults` in `--sources` is misspelled
|
||||
or in the wrong place.
|
||||
- `--ee false` runs the control node's own ansible-core, with only the
|
||||
collections installed here; `ansible-galaxy collection list` shows them.
|
||||
@@ -0,0 +1,30 @@
|
||||
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; }
|
||||
# eff KEY...: a value from navigator's effective settings, under
|
||||
# ansible-navigator:, e.g. eff execution-environment image.
|
||||
eff() {
|
||||
python3 -c 'import sys, yaml
|
||||
v = yaml.safe_load(open(sys.argv[1]))["ansible-navigator"]
|
||||
for k in sys.argv[2:]: v = v[k]
|
||||
print(v)' "$OUT.eff" "$@"
|
||||
}
|
||||
OUT=$LAB_STATE/check.out
|
||||
ans ansible-navigator settings --effective >"$OUT.eff" 2>/dev/null
|
||||
ans ansible-navigator settings --effective --sources -m stdout >"$OUT.src" 2>/dev/null
|
||||
src() { grep -Fqx "ansible-navigator.$1: Settings file" "$OUT.src"; }
|
||||
|
||||
task 1 "ansible-navigator.yml: lab-ee:2.16, never pull, stdout, no artifacts"
|
||||
need "/home/greg/ansible/ansible-navigator.yml exists" 'ls /home/greg/ansible/ansible-navigator.y*ml /home/greg/ansible/ansible-navigator.json 2>/dev/null | grep -q .'
|
||||
need "the image is localhost/lab-ee:2.16, from the settings file" '[ "$(eff execution-environment image)" = localhost/lab-ee:2.16 ] && src execution-environment.image'
|
||||
need "the pull policy is never, from the settings file" '[ "$(eff execution-environment pull policy)" = never ] && src execution-environment.pull.policy'
|
||||
need "the mode is stdout, from the settings file" '[ "$(eff mode)" = stdout ] && grep -Fqx "ansible-navigator.mode: Settings file" <(ans ansible-navigator settings --effective --sources)'
|
||||
need "playbook artifacts are off, from the settings file" '[ "$(eff playbook-artifact enable)" = False ] && src playbook-artifact.enable'
|
||||
need "ansible-navigator run site.yml works on node1 and node2" 'ans ansible-navigator run site.yml >"$OUT"; grep -Eq "^node1 +: ok=[1-9].* failed=0" "$OUT" && grep -Eq "^node2 +: ok=[1-9].* failed=0" "$OUT"'
|
||||
|
||||
task 2 "The error a pull policy of always gives for a local-only image"
|
||||
need "/root/lab-answers/2 has navigator's error" 'grep -qi "execution environment pull failed" /root/lab-answers/2'
|
||||
|
||||
task 3 "The module that fails without the execution environment"
|
||||
need "/root/lab-answers/3 names the module, in full" '[ "$(answer 3)" = community.general.ini_file ]'
|
||||
rm -f "$OUT" "$OUT.eff" "$OUT.src"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.2 ansible-navigator.yml"
|
||||
CHAPTER=/ex294/2-configuring-ansible/2-2-ansible-navigator-yml/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Navigator, if the lab installed it. greg (with his project and his
|
||||
# podman images) goes with users_remove, the nodes with nodes_down.
|
||||
if [ "$(state_get navigator)" = installed ]; then
|
||||
rm -f /usr/local/bin/ansible-navigator
|
||||
rm -rf /opt/le-lab-navigator
|
||||
fi
|
||||
if id greg >/dev/null 2>&1; then
|
||||
runuser -l greg -c 'podman system reset -f' >/dev/null 2>&1
|
||||
fi
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,89 @@
|
||||
# The control node: ansible-core, python3-pip (for ansible-navigator's
|
||||
# virtual environment) and greg with a key and quiet rootless podman.
|
||||
for p in ansible-core python3-pip; do
|
||||
pkg_installed $p || { echo $p >>"$LAB_STATE/missing"; pkg_install $p; }
|
||||
done
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
# Without a systemd user session (su, sudo -i), podman would warn on every
|
||||
# run; the cgroupfs manager works the same in any login.
|
||||
install -d -m 755 -o greg -g greg /home/greg/.config /home/greg/.config/containers
|
||||
printf '[engine]\ncgroup_manager = "cgroupfs"\nevents_logger = "file"\n' >/home/greg/.config/containers/containers.conf
|
||||
chown greg: /home/greg/.config/containers/containers.conf
|
||||
|
||||
# greg's project: ansible.cfg, inventory and a playbook that needs
|
||||
# community.general, which only the image has (task 3). No
|
||||
# ansible-navigator.yml yet (task 1).
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'X'
|
||||
[defaults]
|
||||
inventory = ./inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
X
|
||||
printf '[nodes]\nnode1\nnode2\n' >/home/greg/ansible/inventory
|
||||
cat >/home/greg/ansible/site.yml <<'X'
|
||||
---
|
||||
- name: Configure the managed nodes
|
||||
hosts: all
|
||||
tasks:
|
||||
- name: Make sure the node answers
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Record that Ansible manages the node
|
||||
community.general.ini_file:
|
||||
path: /etc/lab.ini
|
||||
section: lab
|
||||
option: managed_by
|
||||
value: ansible
|
||||
mode: "0644"
|
||||
X
|
||||
chown greg: /home/greg/ansible/*
|
||||
|
||||
# The managed nodes, with devops: greg's key and passwordless sudo.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node "$n" 'useradd -m devops && printf "devops ALL=(ALL) NOPASSWD: ALL\n" >/etc/sudoers.d/devops && chmod 440 /etc/sudoers.d/devops && install -d -m 700 -o devops -g devops /home/devops/.ssh'
|
||||
in_node -i "$n" 'cat >/home/devops/.ssh/authorized_keys && chown devops: /home/devops/.ssh/authorized_keys && chmod 600 /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
|
||||
# ansible-navigator from PyPI, in its own virtual environment. Only the
|
||||
# command goes on the PATH, so ansible-playbook stays the RPM's (and
|
||||
# --ee false runs the control node's ansible-core).
|
||||
if [ ! -e /usr/local/bin/ansible-navigator ]; then
|
||||
say "${C_DIM}Installing ansible-navigator...${C_0}"
|
||||
python3 -m venv /opt/le-lab-navigator
|
||||
for i in 1 2 3; do
|
||||
/opt/le-lab-navigator/bin/pip install -q --disable-pip-version-check 'ansible-navigator==26.10.0' >/dev/null && break
|
||||
sleep 5
|
||||
done
|
||||
ln -s /opt/le-lab-navigator/bin/ansible-navigator /usr/local/bin/ansible-navigator
|
||||
/usr/local/bin/ansible-navigator --version >/dev/null
|
||||
state_set navigator installed
|
||||
fi
|
||||
|
||||
# The execution environment localhost/lab-ee:2.16: the node image with
|
||||
# ansible-core and ansible.posix and community.general from Ansible Galaxy.
|
||||
# Built as root, given to greg's rootless podman, and removed from root's.
|
||||
say "${C_DIM}Building the execution environment image...${C_0}"
|
||||
d=$(mktemp -d)
|
||||
cat >"$d/Containerfile" <<X
|
||||
FROM $LE_NODE_IMAGE
|
||||
LABEL ansible-execution-environment=true
|
||||
RUN dnf -y -q --setopt=install_weak_deps=False install ansible-core \\
|
||||
&& for i in 1 2 3; do \\
|
||||
ansible-galaxy collection install -p /usr/share/ansible/collections \\
|
||||
'ansible.posix:>=2,<3' 'community.general:>=11,<12' && break; sleep 5; \\
|
||||
done \\
|
||||
&& ansible-doc community.general.lvg >/dev/null && ansible-doc ansible.posix.seboolean >/dev/null \\
|
||||
&& dnf clean all
|
||||
X
|
||||
podman build -q --layers=false -t localhost/lab-ee:2.16 "$d" >/dev/null
|
||||
rm -rf "$d"
|
||||
podman save localhost/lab-ee:2.16 | runuser -l greg -c 'podman load -q' >/dev/null 2>&1
|
||||
podman rmi localhost/lab-ee:2.16 >/dev/null
|
||||
runuser -l greg -c 'podman image exists localhost/lab-ee:2.16'
|
||||
@@ -0,0 +1,40 @@
|
||||
# EX294 2.3: Static inventories and groups
|
||||
|
||||
Write an inventory in INI and in YAML, with a group of groups and a group
|
||||
variable, and add the control node so its tasks run locally.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core`. Work as greg,
|
||||
in `/home/greg/ansible`: its `ansible.cfg` reads the inventory
|
||||
`./inventory`, which doesn't exist yet, and logs in as `devops`.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11), `node2` (10.99.1.12) and `node3`
|
||||
(10.99.1.13) are AlmaLinux 10 containers reached over SSH by name. On
|
||||
each, `devops` accepts greg's key. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Write `/home/greg/ansible/inventory` in INI format, with node1 in group
|
||||
`dev`, node2 in `test`, node3 in `prod`, and a group `webservers` that
|
||||
contains the group `prod`. `ansible-inventory --graph` shows node3 under
|
||||
both `prod` and `webservers`, and `ansible webservers --list-hosts`
|
||||
lists node3.
|
||||
2. Write the same inventory in YAML as `/home/greg/ansible/inventory.yml`,
|
||||
with a variable set on the `dev` group.
|
||||
`ansible-inventory -i inventory.yml --graph --vars` shows the same tree,
|
||||
with the variable on node1.
|
||||
3. Add the control node to `/home/greg/ansible/inventory` as `control`, so
|
||||
tasks for it run locally: `ansible control -a hostname` prints this
|
||||
VM's name without SSH.
|
||||
|
||||
## Hints
|
||||
|
||||
- `[group:children]` lists groups; `[group:vars]` sets variables.
|
||||
- In YAML, `hosts:` and `children:` map names to their contents, even when
|
||||
empty: `node1:` with a trailing colon. Group variables go under `vars:`.
|
||||
- `ansible-inventory -y --list` prints an INI inventory as YAML.
|
||||
- `ansible_connection=local` on the host's line.
|
||||
- `ansible all -m ping` checks every node answers.
|
||||
@@ -0,0 +1,36 @@
|
||||
# ans CMD: run CMD as greg in his project directory.
|
||||
ans() { timeout 120 runuser -l greg -c "cd ~/ansible && $*" </dev/null; }
|
||||
# inv FILE PLUGIN: FILE as JSON, read only by that inventory plugin.
|
||||
inv() { ans "ANSIBLE_INVENTORY_ENABLED=$2 ansible-inventory -i $1 --list --export"; }
|
||||
# tree: the JSON on stdin has node1 in dev, node2 in test, node3 in prod
|
||||
# and prod in webservers.
|
||||
tree() {
|
||||
python3 -c 'import json, sys
|
||||
d = json.load(sys.stdin)
|
||||
h = lambda g: d.get(g, {}).get("hosts", [])
|
||||
sys.exit(not ("node1" in h("dev") and "node2" in h("test") and "node3" in h("prod")
|
||||
and "prod" in d.get("webservers", {}).get("children", [])))'
|
||||
}
|
||||
I=/home/greg/ansible/inventory Y=/home/greg/ansible/inventory.yml
|
||||
OUT=$LAB_STATE/check.out
|
||||
|
||||
task 1 "INI inventory: dev, test, prod, and webservers containing prod"
|
||||
need "$I exists" '[ -f "$I" ]'
|
||||
need "$I is an INI inventory" 'inv inventory ini >"$OUT" && grep -q . "$OUT"'
|
||||
need "node1 is in dev, node2 in test, node3 in prod, and prod in webservers" 'tree <"$OUT"'
|
||||
need "ansible webservers --list-hosts lists node3" 'ans ansible webservers --list-hosts | grep -qx " *node3"'
|
||||
|
||||
task 2 "The same inventory in YAML, with a variable on dev"
|
||||
need "$Y exists" '[ -f "$Y" ]'
|
||||
need "$Y is a YAML inventory" 'inv inventory.yml yaml >"$OUT" && grep -q . "$OUT"'
|
||||
need "inventory.yml has node1 in dev, node2 in test, node3 in prod, and prod in webservers" 'tree <"$OUT"'
|
||||
need "inventory.yml sets a variable on the dev group" 'python3 -c "import json, sys; sys.exit(not json.load(open(sys.argv[1])).get(\"dev\", {}).get(\"vars\"))" "$OUT"'
|
||||
need "node1 gets dev's variables" 'ans ansible-inventory -i inventory.yml --host node1 >"$OUT.host" && python3 -c "import json, sys
|
||||
v = json.load(open(sys.argv[1]))[\"dev\"][\"vars\"]; h = json.load(open(sys.argv[2]))
|
||||
sys.exit(not all(h.get(k) == x for k, x in v.items()))" "$OUT" "$OUT.host"'
|
||||
|
||||
task 3 "control in the inventory, run locally"
|
||||
need "the inventory has the host control" 'ans ansible-inventory --host control'
|
||||
need "control has ansible_connection local" 'ans ansible-inventory --host control | python3 -c "import json, sys; sys.exit(json.load(sys.stdin).get(\"ansible_connection\") != \"local\")"'
|
||||
need "ansible control -a hostname prints $(hostname)" 'ans ansible control -o -a hostname | grep -Fq "(stdout) $(hostname)"'
|
||||
rm -f "$OUT" "$OUT.host"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="2.3 Static inventories and groups"
|
||||
CHAPTER=/ex294/2-configuring-ansible/2-3-static-inventories/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,4 @@
|
||||
# The nodes go with nodes_down and greg (with his project) with
|
||||
# users_remove; ansible-core goes if the lab installed it.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,21 @@
|
||||
# The control node: ansible-core, greg with a key, and his project with an
|
||||
# ansible.cfg but no inventory (tasks 1 to 3).
|
||||
pkg_installed ansible-core || { echo ansible-core >>"$LAB_STATE/missing"; pkg_install ansible-core; }
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'X'
|
||||
[defaults]
|
||||
inventory = ./inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
X
|
||||
chown greg: /home/greg/ansible/ansible.cfg
|
||||
|
||||
# The managed nodes, with devops: greg's key and passwordless sudo.
|
||||
nodes_up 3
|
||||
for n in $(nodes_names); do
|
||||
in_node "$n" 'useradd -m devops && printf "devops ALL=(ALL) NOPASSWD: ALL\n" >/etc/sudoers.d/devops && chmod 440 /etc/sudoers.d/devops && install -d -m 700 -o devops -g devops /home/devops/.ssh'
|
||||
in_node -i "$n" 'cat >/home/devops/.ssh/authorized_keys && chown devops: /home/devops/.ssh/authorized_keys && chmod 600 /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,42 @@
|
||||
# EX294 3.2: SSH keys for managed nodes
|
||||
|
||||
Make a key pair for the user who runs Ansible, put its public key on every
|
||||
managed node with a playbook, and trust the nodes' host keys properly
|
||||
instead of turning host key checking off.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`), who has no SSH key yet, and installs `ansible-core` and
|
||||
`sshpass`. Work as greg, in `/home/greg/ansible`: its `ansible.cfg` reads
|
||||
the inventory `inventory`, logs in as `devops`, becomes root, and has
|
||||
`host_key_checking = false`.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name. On each, `devops` has the
|
||||
password `Lab-Pass-1` and passwordless sudo, but no authorized keys; `root`
|
||||
and `ansible` have the same password. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. As greg, create an Ed25519 key pair with no passphrase in the default
|
||||
place, `~/.ssh/id_ed25519`, mode 600.
|
||||
2. Write `/home/greg/ansible/keys.yml`, a playbook that installs greg's
|
||||
public key for `devops` on every managed node, and run it (with `-k` the
|
||||
first time). Then `ssh devops@node1 true` doesn't ask for a password,
|
||||
and a second run of `keys.yml` reports `changed=0`.
|
||||
3. Remove `host_key_checking = false` from `ansible.cfg`, delete greg's
|
||||
`~/.ssh/known_hosts` (SSH filled it in while checking was off), and
|
||||
put the nodes' host keys there with `ssh-keyscan` instead, so that
|
||||
`ansible all -m ping` works with no prompt.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519`.
|
||||
- `ansible.posix.authorized_key` needs `ansible-galaxy collection install
|
||||
ansible.posix`; `ansible.builtin.file` and `lineinfile` do the same job.
|
||||
- `lookup('ansible.builtin.file', '/home/greg/.ssh/id_ed25519.pub')` reads
|
||||
the key on the control node.
|
||||
- `ssh-keyscan node1 node2 >> ~/.ssh/known_hosts`; the names in
|
||||
`known_hosts` must be the ones in the inventory.
|
||||
@@ -0,0 +1,34 @@
|
||||
nodes_wait 60 || true
|
||||
# ans CMD: run CMD as greg in his project directory.
|
||||
ans() { setsid -w timeout 180 runuser -l greg -c "cd ~/ansible && $*" </dev/null; }
|
||||
OUT=$LAB_STATE/check.out
|
||||
K=/home/greg/.ssh/id_ed25519
|
||||
# keyssh NODE: greg logs in to NODE as devops with his key and no password.
|
||||
keyssh() {
|
||||
timeout 20 runuser -u greg -- ssh -i $K -o IdentitiesOnly=yes -o BatchMode=yes \
|
||||
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 devops@"$1" true
|
||||
}
|
||||
# recap0: every node in $OUT's recap has changed=0 and no failures.
|
||||
recap0() { grep -Eq "^node[0-9]+ +: ok=" "$OUT" && ! grep -E " : ok=" "$OUT" | grep -Eq "(changed|unreachable|failed)=[1-9]"; }
|
||||
|
||||
task 1 "An Ed25519 key pair for greg, no passphrase, mode 600"
|
||||
need "/home/greg/.ssh/id_ed25519 and id_ed25519.pub exist" "[ -f $K ] && [ -f $K.pub ]"
|
||||
need "the private key is mode 600" "[ \"\$(stat -c %a $K)\" = 600 ]"
|
||||
need "it's an Ed25519 key" "ssh-keygen -l -f $K.pub | grep -q ED25519"
|
||||
need "it has no passphrase" "ssh-keygen -y -P '' -f $K"
|
||||
|
||||
task 2 "keys.yml puts greg's key in devops's authorized_keys on every node"
|
||||
need "/home/greg/ansible/keys.yml exists" '[ -f /home/greg/ansible/keys.yml ]'
|
||||
for n in $(nodes_names); do
|
||||
need "greg's public key is in /home/devops/.ssh/authorized_keys on $n" "[ -f $K.pub ] && in_node $n cat /home/devops/.ssh/authorized_keys | grep -qF \"\$(cut -d' ' -f2 $K.pub)\""
|
||||
need "greg logs in to $n as devops with his key, no password" "keyssh $n"
|
||||
done
|
||||
need "a second run of keys.yml reports changed=0 and no failures" 'ans ansible-playbook keys.yml -e ansible_password=Lab-Pass-1 -e ansible_become_password=Lab-Pass-1 >"$OUT"; recap0'
|
||||
|
||||
task 3 "Host key checking on, with the nodes' keys in known_hosts"
|
||||
need "ansible.cfg doesn't turn host key checking off" 'ans ansible-config dump | grep -E "^HOST_KEY_CHECKING" | grep -q "= True"'
|
||||
for n in $(nodes_names); do
|
||||
need "greg's ~/.ssh/known_hosts has a host key for $n" "runuser -u greg -- ssh-keygen -F $n -f /home/greg/.ssh/known_hosts"
|
||||
done
|
||||
need "ansible all -m ping works with no prompt (as greg, in ~/ansible)" 'ans ansible all -m ping'
|
||||
rm -f "$OUT"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.2 SSH keys for managed nodes"
|
||||
CHAPTER=/ex294/3-managed-nodes/3-2-ssh-keys/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,4 @@
|
||||
# The nodes go with nodes_down and greg (with his key and project) with
|
||||
# users_remove; the packages the lab installed here.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,28 @@
|
||||
# The control node: ansible-core, sshpass (for -k) and greg's project, but
|
||||
# no key for greg (task 1).
|
||||
for p in ansible-core sshpass; do pkg_installed $p || echo $p >>"$LAB_STATE/missing"; done
|
||||
pkg_installed ansible-core || pkg_install ansible-core
|
||||
pkg_installed sshpass || pkg_install sshpass
|
||||
user_add greg
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'C'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
C
|
||||
printf '[nodes]\nnode1\nnode2\n' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes, with devops (password and sudo, but no keys).
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node "$n" 'id devops >/dev/null 2>&1 || useradd -m devops
|
||||
echo devops:Lab-Pass-1 | chpasswd
|
||||
echo "devops ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/devops
|
||||
chmod 440 /etc/sudoers.d/devops
|
||||
rm -rf /home/devops/.ssh'
|
||||
done
|
||||
@@ -0,0 +1,42 @@
|
||||
# EX294 3.3: Privilege escalation
|
||||
|
||||
Give the automation user passwordless sudo with a validated sudoers
|
||||
drop-in, see `validate` stop a broken one, and run one task as another
|
||||
user in a play that becomes root.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core` and `sshpass`.
|
||||
Work as greg, in `/home/greg/ansible`: its `ansible.cfg` reads the
|
||||
inventory `inventory`, logs in as `devops` and has `become = true`.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name. On each, `devops`
|
||||
accepts greg's key but has no sudo rights yet, so `ansible all -a id`
|
||||
fails for now. `root` and `ansible` (in `wheel`, so sudo asks for the
|
||||
password) have the password `Lab-Pass-1`, as does `devops`. For a root
|
||||
shell on a node, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Write `/home/greg/ansible/sudo.yml`, which gives `devops` passwordless
|
||||
sudo on every managed node with a validated drop-in,
|
||||
`/etc/sudoers.d/devops`, mode 0440, and run it. Then
|
||||
`ansible all -a id` shows `uid=0(root)` on every node.
|
||||
2. Copy it to `/home/greg/ansible/typo.yml` and put a typo in the rule
|
||||
(for example, leave out the colon after `NOPASSWD`). Running
|
||||
`typo.yml` must fail at `validate`, and sudo must still work on the
|
||||
nodes.
|
||||
3. Write `/home/greg/ansible/whoami.yml`: a play with `become: true` whose
|
||||
`command: whoami` task has `become_user: devops` and prints `devops`
|
||||
for every node.
|
||||
|
||||
## Hints
|
||||
|
||||
- Until devops has sudo, log in as someone who has it: `remote_user: root`
|
||||
with `-k`, or `remote_user: ansible` with `-k -K`.
|
||||
- `ansible.builtin.copy` with `content`, `mode: "0440"` and
|
||||
`validate: /usr/sbin/visudo -cf %s`; the rule needs its trailing newline.
|
||||
- `register` the result and print `.stdout` with `debug`, or run the
|
||||
playbook with `-v`.
|
||||
@@ -0,0 +1,36 @@
|
||||
nodes_wait 60 || true
|
||||
# ans CMD: run CMD as greg in his project directory.
|
||||
ans() { setsid -w timeout 180 runuser -l greg -c "cd ~/ansible && $*" </dev/null; }
|
||||
OUT=$LAB_STATE/check.out
|
||||
P='-e ansible_password=Lab-Pass-1 -e ansible_become_password=Lab-Pass-1'
|
||||
A=/home/greg/ansible
|
||||
# sudo_ok NODE: devops's drop-in is valid, mode 0440, and gives NOPASSWD: ALL.
|
||||
sudo_ok() {
|
||||
in_node "$1" 'visudo -cf /etc/sudoers.d/devops && [ "$(stat -c %a /etc/sudoers.d/devops)" = 440 ] &&
|
||||
sudo -l -U devops | grep -q "NOPASSWD: ALL" && runuser -u devops -- sudo -n true'
|
||||
}
|
||||
idroot() { ans ansible all -o -a id >"$OUT" && grep -q . "$OUT" && ! grep -v "uid=0(root)" "$OUT"; }
|
||||
|
||||
task 1 "Passwordless sudo for devops from a validated drop-in, mode 0440"
|
||||
need "/home/greg/ansible/sudo.yml exists" "[ -f $A/sudo.yml ]"
|
||||
need "sudo.yml validates the file with visudo" "grep -Eq 'validate:.*visudo' $A/sudo.yml"
|
||||
for n in $(nodes_names); do
|
||||
need "/etc/sudoers.d/devops on $n is valid, mode 0440, and gives devops NOPASSWD: ALL" "sudo_ok $n"
|
||||
done
|
||||
need "ansible all -a id shows uid=0(root) on every node (as greg, in ~/ansible)" idroot
|
||||
|
||||
task 2 "typo.yml fails at validate, and sudo still works"
|
||||
need "/home/greg/ansible/typo.yml exists" "[ -f $A/typo.yml ]"
|
||||
need "typo.yml validates the file with visudo" "grep -Eq 'validate:.*visudo' $A/typo.yml"
|
||||
need "running typo.yml fails with \"failed to validate\"" "! ans ansible-playbook typo.yml $P >\"\$OUT\" && grep -q 'failed to validate' \"\$OUT\""
|
||||
for n in $(nodes_names); do
|
||||
need "devops still has passwordless sudo on $n" "sudo_ok $n"
|
||||
done
|
||||
need "ansible all -a id still shows uid=0(root) on every node" idroot
|
||||
|
||||
task 3 "whoami.yml runs whoami as devops in a play that becomes root"
|
||||
need "/home/greg/ansible/whoami.yml exists" "[ -f $A/whoami.yml ]"
|
||||
need "whoami.yml has become: true and become_user: devops" "grep -Eq '^ *become: *(true|yes|True)' $A/whoami.yml && grep -Eq '^ *become_user: *[\"'\'']?devops' $A/whoami.yml"
|
||||
need "whoami.yml runs whoami" "grep -q whoami $A/whoami.yml"
|
||||
need "running whoami.yml -v shows whoami printing devops on every node, with no failures" 'ans ansible-playbook whoami.yml -v >"$OUT" && for n in $(nodes_names); do grep -E "^(ok|changed): \[$n\] => \{.*\"stdout\": \"devops\"" "$OUT" >/dev/null || exit 1; done'
|
||||
rm -f "$OUT"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.3 Privilege escalation"
|
||||
CHAPTER=/ex294/3-managed-nodes/3-3-privilege-escalation/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,4 @@
|
||||
# The nodes go with nodes_down and greg (with his key and project) with
|
||||
# users_remove; the packages the lab installed here.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,32 @@
|
||||
# The control node: ansible-core, sshpass (for -k) and greg's project, with
|
||||
# his key.
|
||||
for p in ansible-core sshpass; do pkg_installed $p || echo $p >>"$LAB_STATE/missing"; done
|
||||
pkg_installed ansible-core || pkg_install ansible-core
|
||||
pkg_installed sshpass || pkg_install sshpass
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'C'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
C
|
||||
printf '[nodes]\nnode1\nnode2\n' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes, with devops (password and greg's key, but no sudo).
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'id devops >/dev/null 2>&1 || useradd -m devops
|
||||
echo devops:Lab-Pass-1 | chpasswd
|
||||
rm -f /etc/sudoers.d/devops
|
||||
install -d -m 700 -o devops -g devops /home/devops/.ssh
|
||||
cat >/home/devops/.ssh/authorized_keys
|
||||
chown devops: /home/devops/.ssh/authorized_keys
|
||||
chmod 600 /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,41 @@
|
||||
# EX294 3.4: Deploying files to managed nodes
|
||||
|
||||
Copy a file from the project to the webservers with a backup, make a
|
||||
directory and write a file from inline content, and fetch a file back from
|
||||
every node.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core`. Work as greg,
|
||||
in `/home/greg/ansible`: its `ansible.cfg` reads the inventory `inventory`,
|
||||
whose group `webservers` holds both nodes, logs in as `devops` and becomes
|
||||
root.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name. On each, `devops` has
|
||||
greg's key and passwordless sudo, and `/etc/issue.net` is the old one,
|
||||
owned by `ansible` with mode 0600. SELinux is off inside the nodes, so
|
||||
`ls -lZ` shows no `etc_t` there. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Deploy `/etc/issue.net` to the webservers from a file in your project
|
||||
(such as `files/issue`), owned by root, mode 0644, keeping a backup of
|
||||
the old one. Change the source and run it again: another backup
|
||||
appears.
|
||||
2. On the webservers, create `/srv/app/conf` owned by `devops` with mode
|
||||
0750, and write `app.conf` inside it from inline content.
|
||||
3. Fetch `/etc/hostname` from every webserver into `fetched/` in the
|
||||
project, so that `fetched/node1/etc/hostname` and
|
||||
`fetched/node2/etc/hostname` exist on the control node.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ansible.builtin.copy` with `src`, `dest`, `owner`, `group`, `mode` and
|
||||
`backup: true`; `--diff` shows what changes.
|
||||
- `ansible.builtin.file` with `state: directory`; `copy` with `content`.
|
||||
- `ansible.builtin.fetch` with `dest: fetched/`.
|
||||
- `ansible webservers -a 'stat -c "%U %a" /srv/app/conf'` and
|
||||
`ansible webservers -a 'ls -l /etc'` check your work.
|
||||
@@ -0,0 +1,25 @@
|
||||
nodes_wait 60 || true
|
||||
A=/home/greg/ansible
|
||||
# inproject SUM: some file in greg's project has this SHA-256.
|
||||
inproject() { find $A -path $A/fetched -prune -o -type f -size -1024k -print0 2>/dev/null | xargs -0r sha256sum | grep -q "^$1 "; }
|
||||
sum() { in_node "$1" sha256sum /etc/issue.net | cut -d' ' -f1; }
|
||||
|
||||
task 1 "/etc/issue.net from a project file, root, 0644, with backups"
|
||||
for n in $(nodes_names); do
|
||||
need "/etc/issue.net on $n is owned by root:root with mode 644" "[ \"\$(in_node $n stat -c '%U:%G %a' /etc/issue.net)\" = 'root:root 644' ]"
|
||||
need "/etc/issue.net on $n is no longer the old banner" "! in_node $n grep -q '^Old banner' /etc/issue.net"
|
||||
need "/etc/issue.net on $n is the same as a file in /home/greg/ansible" "inproject \"\$(sum $n)\""
|
||||
need "$n has two backups of /etc/issue.net (/etc/issue.net.*~): one old, one from the change" "[ \"\$(in_node $n 'ls -d /etc/issue.net.*~' | wc -l)\" -ge 2 ]"
|
||||
need "a backup on $n holds the old banner" "in_node $n 'grep -l \"^Old banner\" /etc/issue.net.*~'"
|
||||
done
|
||||
|
||||
task 2 "/srv/app/conf owned by devops, mode 0750, with app.conf"
|
||||
for n in $(nodes_names); do
|
||||
need "/srv/app/conf on $n is a directory owned by devops with mode 750" "[ \"\$(in_node $n stat -c '%F %U %a' /srv/app/conf)\" = 'directory devops 750' ]"
|
||||
need "/srv/app/conf/app.conf on $n has some content" "in_node $n test -s /srv/app/conf/app.conf"
|
||||
done
|
||||
|
||||
task 3 "/etc/hostname fetched from every webserver"
|
||||
for n in $(nodes_names); do
|
||||
need "$A/fetched/$n/etc/hostname holds $n" "[ \"\$(cat $A/fetched/$n/etc/hostname)\" = $n ]"
|
||||
done
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="3.4 Deploying files to managed nodes"
|
||||
CHAPTER=/ex294/3-managed-nodes/3-4-deploying-files/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,4 @@
|
||||
# The nodes go with nodes_down and greg (with his key and project) with
|
||||
# users_remove; the packages the lab installed here.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,37 @@
|
||||
# The control node: ansible-core and greg's project, with his key.
|
||||
for p in ansible-core; do pkg_installed $p || echo $p >>"$LAB_STATE/missing"; done
|
||||
pkg_installed ansible-core || pkg_install ansible-core
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'C'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
C
|
||||
printf '[webservers]\nnode1\nnode2\n' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes: devops with greg's key and sudo, an old issue.net with
|
||||
# the wrong owner and mode, and no /srv/app.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'id devops >/dev/null 2>&1 || useradd -m devops
|
||||
echo devops:Lab-Pass-1 | chpasswd
|
||||
echo "devops ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/devops
|
||||
chmod 440 /etc/sudoers.d/devops
|
||||
install -d -m 700 -o devops -g devops /home/devops/.ssh
|
||||
cat >/home/devops/.ssh/authorized_keys
|
||||
chown devops: /home/devops/.ssh/authorized_keys
|
||||
chmod 600 /home/devops/.ssh/authorized_keys
|
||||
printf "Old banner: authorized use only.\n" >/etc/issue.net
|
||||
chown ansible:ansible /etc/issue.net
|
||||
chmod 600 /etc/issue.net
|
||||
rm -f /etc/issue.net.*~
|
||||
rm -rf /srv/app' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
@@ -0,0 +1,51 @@
|
||||
# EX294 4.1: ansible-playbook and ansible-navigator
|
||||
|
||||
Run the same playbook with ansible-playbook and with ansible-navigator,
|
||||
browse a run in navigator's interactive mode, and see which files an
|
||||
execution environment can read.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core` and
|
||||
`ansible-navigator` (from PyPI, in `/opt/le-lab-navigator`). Work as
|
||||
greg, in `/home/greg/ansible`: its `ansible.cfg` reads the inventory
|
||||
`inventory`, logs in as `devops` and becomes root, and its
|
||||
`ansible-navigator.yml` uses the execution environment image
|
||||
`localhost/lab-ee:2.16` (built on this VM from AlmaLinux 10's ansible-core
|
||||
and collections from Ansible Galaxy; no registry or login) and never
|
||||
pulls. It doesn't set the mode, so navigator starts in interactive mode
|
||||
unless you give `-m stdout`.
|
||||
|
||||
`site.yml` checks every node and adds a line to the node's
|
||||
`/var/log/lab-runs.log` saying which command ran it, where Ansible ran,
|
||||
and the `--limit`. The lab also leaves a note for task 3 in
|
||||
`/tmp/lab-note.txt`.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name; on both, `devops`
|
||||
accepts greg's key and may use sudo without a password. For a root shell
|
||||
on a node, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Run `site.yml` with `ansible-playbook`, then with `ansible-navigator
|
||||
run` in stdout mode, limited to one node. Both recaps show the same
|
||||
counts for that node.
|
||||
2. Run `site.yml` with navigator in interactive mode and open the result
|
||||
of one task. Not graded: check that the task screen shows `changed:`
|
||||
and the module's arguments under `invocation`.
|
||||
3. Write `/home/greg/ansible/note.yml`, a playbook for `localhost` that
|
||||
prints the note's contents with a `file` lookup, and run it with both
|
||||
commands. ansible-playbook prints it from `/tmp`; navigator can't find
|
||||
it there. Copy the note into the project and point the lookup at the
|
||||
copy, so both commands print it.
|
||||
|
||||
## Hints
|
||||
|
||||
- `--limit node1`, `-m stdout`. In interactive mode, type a row's number
|
||||
(`:0`) to open it, Esc to go back, `:q` to quit.
|
||||
- `lookup('ansible.builtin.file', 'PATH')` in a `debug` task's `msg`.
|
||||
- The execution environment sees the project directory and `~/.ssh`, not
|
||||
the rest of the control node. A relative path in a `file` lookup is
|
||||
looked for next to the playbook (and in `files/`).
|
||||
@@ -0,0 +1,21 @@
|
||||
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; }
|
||||
OUT=$LAB_STATE/check.out Q="'"
|
||||
for n in $(nodes_names); do in_node "$n" cat /var/log/lab-runs.log 2>/dev/null; done >"$OUT.runs"
|
||||
N=$(state_get note)
|
||||
|
||||
task 1 "site.yml run with ansible-playbook, and with navigator limited to one node"
|
||||
need "a node logged a run of site.yml by ansible-playbook" 'grep -q " ansible-playbook in the control node, " "$OUT.runs"'
|
||||
need "a node logged a run of site.yml by ansible-navigator in the execution environment, with --limit" 'grep " ansible-navigator in an execution environment, limit " "$OUT.runs" | grep -vq "limit none$"'
|
||||
|
||||
task 2 "Open a task's result in navigator's interactive mode" self
|
||||
|
||||
task 3 "note.yml prints the note with a file lookup, with both commands"
|
||||
P=/home/greg/ansible/note.yml
|
||||
need "$P exists" '[ -f "$P" ]'
|
||||
need "note.yml uses a file lookup" 'grep -Eq "(lookup|query|q)\( *[\"$Q](ansible\.builtin\.)?file[\"$Q]" "$P"'
|
||||
need "a copy of the note is in /home/greg/ansible" 'grep -rlqF "$N" /home/greg/ansible --exclude="*.log" --exclude="*.json"'
|
||||
need "ansible-playbook note.yml prints the note" 'ans ansible-playbook note.yml | grep -qF "$N"'
|
||||
need "ansible-navigator run note.yml -m stdout prints the note" 'ans ansible-navigator run note.yml -m stdout | grep -qF "$N"'
|
||||
rm -f "$OUT" "$OUT.runs"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="4.1 ansible-playbook and ansible-navigator"
|
||||
CHAPTER=/ex294/4-running-playbooks/4-1-ansible-playbook-and-navigator/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,12 @@
|
||||
# Navigator, if the lab installed it. greg (with his project and his
|
||||
# podman images) goes with users_remove, the nodes with nodes_down.
|
||||
if [ "$(state_get navigator)" = installed ]; then
|
||||
rm -f /usr/local/bin/ansible-navigator
|
||||
rm -rf /opt/le-lab-navigator
|
||||
fi
|
||||
if id greg >/dev/null 2>&1; then
|
||||
runuser -l greg -c 'podman system reset -f' >/dev/null 2>&1
|
||||
fi
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
rm -f /tmp/lab-note.txt
|
||||
true
|
||||
@@ -0,0 +1,107 @@
|
||||
# The control node: ansible-core, python3-pip (for ansible-navigator's
|
||||
# virtual environment) and greg with a key and quiet rootless podman.
|
||||
for p in ansible-core python3-pip; do
|
||||
pkg_installed $p || { echo $p >>"$LAB_STATE/missing"; pkg_install $p; }
|
||||
done
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
# Without a systemd user session (su, sudo -i), podman would warn on every
|
||||
# run; the cgroupfs manager works the same in any login.
|
||||
install -d -m 755 -o greg -g greg /home/greg/.config /home/greg/.config/containers
|
||||
printf '[engine]\ncgroup_manager = "cgroupfs"\nevents_logger = "file"\n' >/home/greg/.config/containers/containers.conf
|
||||
chown greg: /home/greg/.config/containers/containers.conf
|
||||
|
||||
# greg's project: ansible.cfg, inventory, ansible-navigator.yml without a
|
||||
# mode, and site.yml, which records each run on the node (task 1).
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'X'
|
||||
[defaults]
|
||||
inventory = ./inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
X
|
||||
printf '[nodes]\nnode1\nnode2\n' >/home/greg/ansible/inventory
|
||||
cat >/home/greg/ansible/ansible-navigator.yml <<'X'
|
||||
---
|
||||
ansible-navigator:
|
||||
execution-environment:
|
||||
image: localhost/lab-ee:2.16
|
||||
pull:
|
||||
policy: never
|
||||
playbook-artifact:
|
||||
enable: false
|
||||
X
|
||||
cat >/home/greg/ansible/site.yml <<'X'
|
||||
---
|
||||
- name: Check the managed nodes
|
||||
hosts: all
|
||||
vars:
|
||||
run_by: "{{ 'ansible-navigator' if lookup('ansible.builtin.env', 'ANSIBLE_STDOUT_CALLBACK') == 'awx_display' else 'ansible-playbook' }}"
|
||||
run_in: "{{ 'an execution environment' if lookup('ansible.builtin.env', 'container') else 'the control node' }}"
|
||||
tasks:
|
||||
- name: Make sure the node answers
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Record this run in /var/log/lab-runs.log
|
||||
ansible.builtin.lineinfile:
|
||||
path: /var/log/lab-runs.log
|
||||
line: "{{ now(utc=true, fmt='%Y-%m-%d %H:%M:%S') }} {{ run_by }} in {{ run_in }}, limit {{ ansible_limit | default('none') }}"
|
||||
create: true
|
||||
mode: "0644"
|
||||
changed_when: false
|
||||
X
|
||||
chown greg: /home/greg/ansible/*
|
||||
|
||||
# The note for task 3, outside the project.
|
||||
N=$(od -An -N6 -tx1 /dev/urandom | tr -d ' \n')
|
||||
state_set note "$N"
|
||||
printf 'The note says: %s\n' "$N" >/tmp/lab-note.txt
|
||||
chown greg: /tmp/lab-note.txt
|
||||
|
||||
# The managed nodes, with devops: greg's key and passwordless sudo.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node "$n" 'useradd -m devops && printf "devops ALL=(ALL) NOPASSWD: ALL\n" >/etc/sudoers.d/devops && chmod 440 /etc/sudoers.d/devops && install -d -m 700 -o devops -g devops /home/devops/.ssh'
|
||||
in_node -i "$n" 'cat >/home/devops/.ssh/authorized_keys && chown devops: /home/devops/.ssh/authorized_keys && chmod 600 /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
|
||||
# ansible-navigator from PyPI, in its own virtual environment. Only the
|
||||
# command goes on the PATH, so ansible-playbook stays the RPM's (and
|
||||
# --ee false runs the control node's ansible-core).
|
||||
if [ ! -e /usr/local/bin/ansible-navigator ]; then
|
||||
say "${C_DIM}Installing ansible-navigator...${C_0}"
|
||||
python3 -m venv /opt/le-lab-navigator
|
||||
for i in 1 2 3; do
|
||||
/opt/le-lab-navigator/bin/pip install -q --disable-pip-version-check 'ansible-navigator==26.10.0' >/dev/null && break
|
||||
sleep 5
|
||||
done
|
||||
ln -s /opt/le-lab-navigator/bin/ansible-navigator /usr/local/bin/ansible-navigator
|
||||
/usr/local/bin/ansible-navigator --version >/dev/null
|
||||
state_set navigator installed
|
||||
fi
|
||||
|
||||
# The execution environment localhost/lab-ee:2.16: the node image with
|
||||
# ansible-core and ansible.posix and community.general from Ansible Galaxy.
|
||||
# Built as root, given to greg's rootless podman, and removed from root's.
|
||||
say "${C_DIM}Building the execution environment image...${C_0}"
|
||||
d=$(mktemp -d)
|
||||
cat >"$d/Containerfile" <<X
|
||||
FROM $LE_NODE_IMAGE
|
||||
LABEL ansible-execution-environment=true
|
||||
RUN dnf -y -q --setopt=install_weak_deps=False install ansible-core \\
|
||||
&& for i in 1 2 3; do \\
|
||||
ansible-galaxy collection install -p /usr/share/ansible/collections \\
|
||||
'ansible.posix:>=2,<3' 'community.general:>=11,<12' && break; sleep 5; \\
|
||||
done \\
|
||||
&& ansible-doc community.general.lvg >/dev/null && ansible-doc ansible.posix.seboolean >/dev/null \\
|
||||
&& dnf clean all
|
||||
X
|
||||
podman build -q --layers=false -t localhost/lab-ee:2.16 "$d" >/dev/null
|
||||
rm -rf "$d"
|
||||
podman save localhost/lab-ee:2.16 | runuser -l greg -c 'podman load -q' >/dev/null 2>&1
|
||||
podman rmi localhost/lab-ee:2.16 >/dev/null
|
||||
runuser -l greg -c 'podman image exists localhost/lab-ee:2.16'
|
||||
@@ -0,0 +1,50 @@
|
||||
# EX294 4.2: Finding modules with ansible-navigator
|
||||
|
||||
Browse the collections in an execution environment, use a module from its
|
||||
documentation, and see a module that only the image has.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core` and
|
||||
`ansible-navigator` (from PyPI, in `/opt/le-lab-navigator`). Work as
|
||||
greg, in `/home/greg/ansible`: its `ansible.cfg` reads the inventory
|
||||
`inventory`, logs in as `devops` and becomes root, and its
|
||||
`ansible-navigator.yml` uses the execution environment image
|
||||
`localhost/lab-ee:2.16` in stdout mode. The image is built on this VM
|
||||
from AlmaLinux 10's ansible-core with the `ansible.posix` and
|
||||
`community.general` collections from Ansible Galaxy; the control node
|
||||
itself has only ansible-core's own modules.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name; on both, `devops`
|
||||
accepts greg's key and may use sudo without a password. For a root shell
|
||||
on a node, `sudo podman exec -it node1 bash`.
|
||||
|
||||
The nodes have no disks of their own, so storage work goes to this VM:
|
||||
the inventory group `storage` holds `control`, which is this VM reached
|
||||
over SSH at 10.99.1.1 (inside the execution environment, `localhost` is
|
||||
the container, not this VM). There too `devops` accepts greg's key and
|
||||
may use sudo without a password. The VM has a spare disk, `/dev/loop41`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. List the collections in the execution environment and find the module
|
||||
that manages SELinux booleans. Write its full name to
|
||||
`/root/lab-answers/1` (as root).
|
||||
2. Read `community.general.lvg`'s documentation from the image, and write
|
||||
a playbook in `/home/greg/ansible` with a task that creates the volume
|
||||
group `data` on `/dev/loop41` for the `storage` group. Run it with
|
||||
`ansible-navigator run`; `vgs` on this VM shows `data`.
|
||||
3. Find a module that's in the image but not on the control node, and
|
||||
write `/home/greg/ansible/both.yml`, a playbook that uses it. Run it
|
||||
both ways: navigator succeeds, ansible-playbook fails with "couldn't
|
||||
resolve module/action".
|
||||
|
||||
## Hints
|
||||
|
||||
- `ansible-navigator collections -m interactive` browses them; in stdout
|
||||
mode it prints every plugin as YAML, so pipe it through `grep`.
|
||||
`ansible-navigator doc -l`, `ansible-navigator doc NAME`.
|
||||
- Use modules by their full name, `namespace.collection.module`.
|
||||
- `ansible-doc -l` lists what the control node has.
|
||||
@@ -0,0 +1,19 @@
|
||||
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; }
|
||||
OUT=$LAB_STATE/check.out
|
||||
|
||||
task 1 "The module that manages SELinux booleans"
|
||||
need "/root/lab-answers/1 names it in full" '[ "$(answer 1)" = ansible.posix.seboolean ]'
|
||||
|
||||
task 2 "A community.general.lvg task makes volume group data on /dev/loop41"
|
||||
need "a playbook in /home/greg/ansible uses community.general.lvg" 'grep -lsq "community\.general\.lvg" /home/greg/ansible/*.yml /home/greg/ansible/*.yaml'
|
||||
need "volume group data exists" 'vgs data'
|
||||
need "data's physical volume is /dev/loop41" 'pvs --noheadings -o pv_name,vg_name | grep -Eq "^ */dev/loop41 +data *$"'
|
||||
|
||||
task 3 "both.yml: a module only the image has"
|
||||
B=/home/greg/ansible/both.yml
|
||||
need "$B exists" '[ -f "$B" ]'
|
||||
need "ansible-navigator run both.yml --syntax-check passes" 'ans ansible-navigator run both.yml --syntax-check -m stdout'
|
||||
need "ansible-playbook both.yml --syntax-check fails with couldn't resolve module/action" 'ans ansible-playbook both.yml --syntax-check >"$OUT" 2>&1; grep -q "couldn.t resolve module/action" "$OUT"'
|
||||
rm -f "$OUT"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="4.2 Finding modules with ansible-navigator"
|
||||
CHAPTER=/ex294/4-running-playbooks/4-2-finding-modules/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,13 @@
|
||||
# Navigator, if the lab installed it. greg (with his project and his
|
||||
# podman images) goes with users_remove, the nodes with nodes_down.
|
||||
if [ "$(state_get navigator)" = installed ]; then
|
||||
rm -f /usr/local/bin/ansible-navigator
|
||||
rm -rf /opt/le-lab-navigator
|
||||
fi
|
||||
if id greg >/dev/null 2>&1; then
|
||||
runuser -l greg -c 'podman system reset -f' >/dev/null 2>&1
|
||||
fi
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
# devops and the spare disk go with users_remove and disks_remove.
|
||||
rm -f /etc/sudoers.d/le-lab-devops
|
||||
true
|
||||
@@ -0,0 +1,97 @@
|
||||
# Task 2 needs LVM on this VM.
|
||||
command -v vgs >/dev/null || refuse "lvm2 isn't installed; install it (dnf install lvm2) and start again"
|
||||
vgs data >/dev/null 2>&1 && refuse "this VM already has a volume group data; remove or rename it and start again"
|
||||
|
||||
# The control node: ansible-core, python3-pip (for ansible-navigator's
|
||||
# virtual environment) and greg with a key and quiet rootless podman.
|
||||
for p in ansible-core python3-pip; do
|
||||
pkg_installed $p || { echo $p >>"$LAB_STATE/missing"; pkg_install $p; }
|
||||
done
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
# Without a systemd user session (su, sudo -i), podman would warn on every
|
||||
# run; the cgroupfs manager works the same in any login.
|
||||
install -d -m 755 -o greg -g greg /home/greg/.config /home/greg/.config/containers
|
||||
printf '[engine]\ncgroup_manager = "cgroupfs"\nevents_logger = "file"\n' >/home/greg/.config/containers/containers.conf
|
||||
chown greg: /home/greg/.config/containers/containers.conf
|
||||
|
||||
# greg's project: ansible.cfg, inventory with the nodes and this VM, and
|
||||
# ansible-navigator.yml.
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'X'
|
||||
[defaults]
|
||||
inventory = ./inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
X
|
||||
printf '[nodes]\nnode1\nnode2\n\n[storage]\ncontrol ansible_host=10.99.1.1\n' >/home/greg/ansible/inventory
|
||||
cat >/home/greg/ansible/ansible-navigator.yml <<'X'
|
||||
---
|
||||
ansible-navigator:
|
||||
execution-environment:
|
||||
image: localhost/lab-ee:2.16
|
||||
pull:
|
||||
policy: never
|
||||
mode: stdout
|
||||
playbook-artifact:
|
||||
enable: false
|
||||
X
|
||||
chown greg: /home/greg/ansible/*
|
||||
|
||||
# This VM as a managed host: devops with greg's key and passwordless sudo,
|
||||
# and a spare disk (task 2).
|
||||
user_add devops
|
||||
printf 'devops ALL=(ALL) NOPASSWD: ALL\n' >/etc/sudoers.d/le-lab-devops
|
||||
chmod 440 /etc/sudoers.d/le-lab-devops
|
||||
install -d -m 700 -o devops -g devops /home/devops/.ssh
|
||||
install -m 600 -o devops -g devops /home/greg/.ssh/id_ed25519.pub /home/devops/.ssh/authorized_keys
|
||||
restorecon -R /home/devops/.ssh 2>/dev/null || true
|
||||
disk_add 1 300M >/dev/null
|
||||
|
||||
# The managed nodes, with devops: greg's key and passwordless sudo.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node "$n" 'useradd -m devops && printf "devops ALL=(ALL) NOPASSWD: ALL\n" >/etc/sudoers.d/devops && chmod 440 /etc/sudoers.d/devops && install -d -m 700 -o devops -g devops /home/devops/.ssh'
|
||||
in_node -i "$n" 'cat >/home/devops/.ssh/authorized_keys && chown devops: /home/devops/.ssh/authorized_keys && chmod 600 /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
|
||||
# ansible-navigator from PyPI, in its own virtual environment. Only the
|
||||
# command goes on the PATH, so ansible-playbook stays the RPM's (and
|
||||
# --ee false runs the control node's ansible-core).
|
||||
if [ ! -e /usr/local/bin/ansible-navigator ]; then
|
||||
say "${C_DIM}Installing ansible-navigator...${C_0}"
|
||||
python3 -m venv /opt/le-lab-navigator
|
||||
for i in 1 2 3; do
|
||||
/opt/le-lab-navigator/bin/pip install -q --disable-pip-version-check 'ansible-navigator==26.10.0' >/dev/null && break
|
||||
sleep 5
|
||||
done
|
||||
ln -s /opt/le-lab-navigator/bin/ansible-navigator /usr/local/bin/ansible-navigator
|
||||
/usr/local/bin/ansible-navigator --version >/dev/null
|
||||
state_set navigator installed
|
||||
fi
|
||||
|
||||
# The execution environment localhost/lab-ee:2.16: the node image with
|
||||
# ansible-core and ansible.posix and community.general from Ansible Galaxy.
|
||||
# Built as root, given to greg's rootless podman, and removed from root's.
|
||||
say "${C_DIM}Building the execution environment image...${C_0}"
|
||||
d=$(mktemp -d)
|
||||
cat >"$d/Containerfile" <<X
|
||||
FROM $LE_NODE_IMAGE
|
||||
LABEL ansible-execution-environment=true
|
||||
RUN dnf -y -q --setopt=install_weak_deps=False install ansible-core \\
|
||||
&& for i in 1 2 3; do \\
|
||||
ansible-galaxy collection install -p /usr/share/ansible/collections \\
|
||||
'ansible.posix:>=2,<3' 'community.general:>=11,<12' && break; sleep 5; \\
|
||||
done \\
|
||||
&& ansible-doc community.general.lvg >/dev/null && ansible-doc ansible.posix.seboolean >/dev/null \\
|
||||
&& dnf clean all
|
||||
X
|
||||
podman build -q --layers=false -t localhost/lab-ee:2.16 "$d" >/dev/null
|
||||
rm -rf "$d"
|
||||
podman save localhost/lab-ee:2.16 | runuser -l greg -c 'podman load -q' >/dev/null 2>&1
|
||||
podman rmi localhost/lab-ee:2.16 >/dev/null
|
||||
runuser -l greg -c 'podman image exists localhost/lab-ee:2.16'
|
||||
@@ -0,0 +1,49 @@
|
||||
# EX294 4.3: Navigator inventories and settings
|
||||
|
||||
Make ansible-navigator see the same inventory and `ansible.cfg` as
|
||||
Ansible does, and give its execution environment a roles directory from
|
||||
outside the project.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core` and
|
||||
`ansible-navigator` (from PyPI, in `/opt/le-lab-navigator`). Work as
|
||||
greg, in `/home/greg/ansible`: its `ansible.cfg` reads the inventory
|
||||
`inventory`, logs in as `devops` and becomes root. Its
|
||||
`ansible-navigator.yml` uses the execution environment image
|
||||
`localhost/lab-ee:2.16` (built on this VM from AlmaLinux 10's ansible-core
|
||||
and collections from Ansible Galaxy; no registry or login) in stdout mode,
|
||||
but someone left two more settings in it.
|
||||
|
||||
greg also has a role, `banner`, in `/home/greg/roles`, outside the
|
||||
project, and the project's `banner.yml` applies it to every node.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name; on both, `devops`
|
||||
accepts greg's key and may use sudo without a password. For a root shell
|
||||
on a node, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Show the inventory's groups with ansible-navigator, in stdout mode and
|
||||
in the interactive browser, and make them match: `ansible-navigator
|
||||
inventory -m stdout --graph` prints the same as `ansible-inventory
|
||||
--graph`.
|
||||
2. Find which `ansible.cfg` ansible-navigator uses, and make it the
|
||||
project's: `ansible-navigator config dump --only-changed -m stdout`
|
||||
shows `CONFIG_FILE() = /home/greg/ansible/ansible.cfg`.
|
||||
3. Set `roles_path` in the project's `ansible.cfg` to `/home/greg/roles`
|
||||
and run `banner.yml` with navigator: it fails with "the role 'banner'
|
||||
was not found". Fix it with a volume mount in `ansible-navigator.yml`,
|
||||
so the run succeeds and every node has `/etc/motd.d/banner`.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ansible-navigator settings --effective --sources -m stdout` says where
|
||||
each setting came from: look at `ansible.config.path` and
|
||||
`ansible.inventory.entries`.
|
||||
- `ansible-navigator inventory -m interactive`: numbers open a view, Esc
|
||||
goes back, `:q` quits.
|
||||
- `execution-environment: volume-mounts:` takes a list of `src`, `dest`
|
||||
and `options` (`Z` relabels the directory for SELinux).
|
||||
@@ -0,0 +1,24 @@
|
||||
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; }
|
||||
OUT=$LAB_STATE/check.out
|
||||
C=/home/greg/ansible/ansible.cfg
|
||||
|
||||
task 1 "ansible-navigator inventory --graph matches ansible-inventory --graph"
|
||||
need "ansible-inventory --graph works" 'ans ansible-inventory --graph >"$OUT.a" && grep -q node1 "$OUT.a"'
|
||||
need "ansible-navigator inventory -m stdout --graph prints the same groups" 'ans ansible-navigator inventory -m stdout --graph >"$OUT.n" && diff -q "$OUT.a" "$OUT.n"'
|
||||
|
||||
task 2 "ansible-navigator uses the project's ansible.cfg"
|
||||
ans ansible-navigator config dump --only-changed -m stdout >"$OUT.cfg" 2>/dev/null
|
||||
need "the config dump shows CONFIG_FILE() = $C" 'grep -Fqx "CONFIG_FILE() = $C" "$OUT.cfg"'
|
||||
|
||||
task 3 "roles_path outside the project, mounted into the execution environment"
|
||||
need "$C sets roles_path to /home/greg/roles" 'ans ansible-config dump --only-changed | grep -F "DEFAULT_ROLES_PATH($C) = " | grep -q "/home/greg/roles\b"'
|
||||
need "the execution environment mounts /home/greg/roles" 'ans ansible-navigator settings --effective -m stdout | python3 -c "import sys, yaml
|
||||
m = yaml.safe_load(sys.stdin)[\"ansible-navigator\"][\"execution-environment\"].get(\"volume-mounts\") or []
|
||||
sys.exit(not any(str(v.get(\"src\", \"\")).rstrip(\"/\") == \"/home/greg/roles\" for v in m))"'
|
||||
need "ansible-navigator finds the banner role (banner.yml --syntax-check)" 'ans ansible-navigator run banner.yml --syntax-check -m stdout'
|
||||
for n in $(nodes_names); do
|
||||
need "$n has /etc/motd.d/banner from the role" "in_node $n grep -q 'managed by Ansible' /etc/motd.d/banner"
|
||||
done
|
||||
rm -f "$OUT".*
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="4.3 Navigator inventories and settings"
|
||||
CHAPTER=/ex294/4-running-playbooks/4-3-navigator-inventories-and-settings/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Navigator, if the lab installed it. greg (with his project and his
|
||||
# podman images) goes with users_remove, the nodes with nodes_down.
|
||||
if [ "$(state_get navigator)" = installed ]; then
|
||||
rm -f /usr/local/bin/ansible-navigator
|
||||
rm -rf /opt/le-lab-navigator
|
||||
fi
|
||||
if id greg >/dev/null 2>&1; then
|
||||
runuser -l greg -c 'podman system reset -f' >/dev/null 2>&1
|
||||
fi
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,117 @@
|
||||
# The control node: ansible-core, python3-pip (for ansible-navigator's
|
||||
# virtual environment) and greg with a key and quiet rootless podman.
|
||||
for p in ansible-core python3-pip; do
|
||||
pkg_installed $p || { echo $p >>"$LAB_STATE/missing"; pkg_install $p; }
|
||||
done
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
# Without a systemd user session (su, sudo -i), podman would warn on every
|
||||
# run; the cgroupfs manager works the same in any login.
|
||||
install -d -m 755 -o greg -g greg /home/greg/.config /home/greg/.config/containers
|
||||
printf '[engine]\ncgroup_manager = "cgroupfs"\nevents_logger = "file"\n' >/home/greg/.config/containers/containers.conf
|
||||
chown greg: /home/greg/.config/containers/containers.conf
|
||||
|
||||
# greg's project. ansible-navigator.yml also names another inventory
|
||||
# (task 1) and another ansible.cfg (task 2); the role lives outside the
|
||||
# project (task 3).
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible /home/greg/ansible/old
|
||||
cat >/home/greg/ansible/ansible.cfg <<'X'
|
||||
[defaults]
|
||||
inventory = ./inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
X
|
||||
printf '[webservers]\nnode1\n\n[dbservers]\nnode2\n' >/home/greg/ansible/inventory
|
||||
printf '[web]\nnode1\nnode2\n' >/home/greg/ansible/old/inventory
|
||||
cat >/home/greg/ansible/old/ansible.cfg <<'X'
|
||||
[defaults]
|
||||
inventory = ./inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
forks = 2
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
X
|
||||
cat >/home/greg/ansible/ansible-navigator.yml <<'X'
|
||||
---
|
||||
ansible-navigator:
|
||||
ansible:
|
||||
config:
|
||||
path: ./old/ansible.cfg
|
||||
inventory:
|
||||
entries:
|
||||
- ./old/inventory
|
||||
execution-environment:
|
||||
image: localhost/lab-ee:2.16
|
||||
pull:
|
||||
policy: never
|
||||
mode: stdout
|
||||
playbook-artifact:
|
||||
enable: false
|
||||
X
|
||||
cat >/home/greg/ansible/banner.yml <<'X'
|
||||
---
|
||||
- name: Put up the banner
|
||||
hosts: all
|
||||
roles:
|
||||
- banner
|
||||
X
|
||||
install -d -m 755 -o greg -g greg /home/greg/roles /home/greg/roles/banner /home/greg/roles/banner/tasks
|
||||
cat >/home/greg/roles/banner/tasks/main.yml <<'X'
|
||||
---
|
||||
- name: Write the banner
|
||||
ansible.builtin.copy:
|
||||
content: "This node is managed by Ansible.\n"
|
||||
dest: /etc/motd.d/banner
|
||||
mode: "0644"
|
||||
X
|
||||
chown -R greg: /home/greg/ansible /home/greg/roles
|
||||
|
||||
# The managed nodes, with devops: greg's key and passwordless sudo.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node "$n" 'useradd -m devops && printf "devops ALL=(ALL) NOPASSWD: ALL\n" >/etc/sudoers.d/devops && chmod 440 /etc/sudoers.d/devops && install -d -m 700 -o devops -g devops /home/devops/.ssh'
|
||||
in_node -i "$n" 'cat >/home/devops/.ssh/authorized_keys && chown devops: /home/devops/.ssh/authorized_keys && chmod 600 /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
|
||||
# ansible-navigator from PyPI, in its own virtual environment. Only the
|
||||
# command goes on the PATH, so ansible-playbook stays the RPM's (and
|
||||
# --ee false runs the control node's ansible-core).
|
||||
if [ ! -e /usr/local/bin/ansible-navigator ]; then
|
||||
say "${C_DIM}Installing ansible-navigator...${C_0}"
|
||||
python3 -m venv /opt/le-lab-navigator
|
||||
for i in 1 2 3; do
|
||||
/opt/le-lab-navigator/bin/pip install -q --disable-pip-version-check 'ansible-navigator==26.10.0' >/dev/null && break
|
||||
sleep 5
|
||||
done
|
||||
ln -s /opt/le-lab-navigator/bin/ansible-navigator /usr/local/bin/ansible-navigator
|
||||
/usr/local/bin/ansible-navigator --version >/dev/null
|
||||
state_set navigator installed
|
||||
fi
|
||||
|
||||
# The execution environment localhost/lab-ee:2.16: the node image with
|
||||
# ansible-core and ansible.posix and community.general from Ansible Galaxy.
|
||||
# Built as root, given to greg's rootless podman, and removed from root's.
|
||||
say "${C_DIM}Building the execution environment image...${C_0}"
|
||||
d=$(mktemp -d)
|
||||
cat >"$d/Containerfile" <<X
|
||||
FROM $LE_NODE_IMAGE
|
||||
LABEL ansible-execution-environment=true
|
||||
RUN dnf -y -q --setopt=install_weak_deps=False install ansible-core \\
|
||||
&& for i in 1 2 3; do \\
|
||||
ansible-galaxy collection install -p /usr/share/ansible/collections \\
|
||||
'ansible.posix:>=2,<3' 'community.general:>=11,<12' && break; sleep 5; \\
|
||||
done \\
|
||||
&& ansible-doc community.general.lvg >/dev/null && ansible-doc ansible.posix.seboolean >/dev/null \\
|
||||
&& dnf clean all
|
||||
X
|
||||
podman build -q --layers=false -t localhost/lab-ee:2.16 "$d" >/dev/null
|
||||
rm -rf "$d"
|
||||
podman save localhost/lab-ee:2.16 | runuser -l greg -c 'podman load -q' >/dev/null 2>&1
|
||||
podman rmi localhost/lab-ee:2.16 >/dev/null
|
||||
runuser -l greg -c 'podman image exists localhost/lab-ee:2.16'
|
||||
@@ -0,0 +1,49 @@
|
||||
# EX294 5.1: Cloning a repository
|
||||
|
||||
Make a bare Git repository on a managed node, clone it on the control node
|
||||
over SSH, set your Git identity and push a first commit, and clone it a
|
||||
second time.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core` and `git`. Work
|
||||
as greg. His Ansible project is `/home/greg/ansible`: its `ansible.cfg`
|
||||
reads the inventory `inventory`, logs in as `devops` and becomes root.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name, with `git` installed. On
|
||||
each, `devops` has greg's key and passwordless sudo, so
|
||||
`ssh devops@node1` works without a password. For a root shell on a node,
|
||||
`sudo podman exec -it node1 bash`. Everything stays on this VM: the
|
||||
repository lives on a node, not on a hosting service.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Make a bare repository `/srv/git/playbooks.git` with the branch `main`
|
||||
on `node1` (`git init --bare -b main /srv/git/playbooks.git`, with
|
||||
Ansible or in a shell there). Then, as greg, set `init.defaultBranch`
|
||||
to `main` and clone it over SSH into `/home/greg/playbooks`, so that
|
||||
`git remote -v` there shows the URL for fetch and push, and
|
||||
`git status` says "On branch main".
|
||||
2. Set greg's Git name and email globally, then commit a `README.md` in
|
||||
the clone and push it, so that `git log` shows them as the author.
|
||||
3. Clone the same repository again, into a directory of your choosing in
|
||||
greg's home, so that it has the same `git log --oneline` as the first
|
||||
clone.
|
||||
|
||||
## Hints
|
||||
|
||||
- `ansible node1 -a 'git init --bare -b main /srv/git/playbooks.git'`
|
||||
makes it as root; for `devops` to push to it, give it to devops with
|
||||
`ansible.builtin.file` and `recurse: true`.
|
||||
- `git clone devops@node1:/srv/git/playbooks.git`; the first SSH
|
||||
connection asks you to trust node1's host key.
|
||||
- An empty repository clones with a warning. Over SSH, its clone starts
|
||||
on Git's own default branch, `master`, unless
|
||||
`git config --global init.defaultBranch main` comes first; in a clone
|
||||
that's already on `master`, `git switch -c main` before the first
|
||||
commit.
|
||||
- The first push of a new branch: `git push -u origin main`.
|
||||
- `git config --global user.name ...`, `user.email`, `--list`;
|
||||
`git log --format='%an <%ae>'` shows the author.
|
||||
@@ -0,0 +1,40 @@
|
||||
nodes_wait 60 || true
|
||||
R=/srv/git/playbooks.git
|
||||
C=/home/greg/playbooks
|
||||
# g CMD: run git CMD as greg, over SSH with his key and no prompts.
|
||||
g() {
|
||||
setsid -w timeout 60 runuser -u greg -- env HOME=/home/greg \
|
||||
GIT_SSH_COMMAND="ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10" \
|
||||
git "$@" </dev/null
|
||||
}
|
||||
# origin_ok DIR: DIR is a clone whose origin is node1's repository over SSH.
|
||||
origin_ok() {
|
||||
g -C "$1" remote get-url origin 2>/dev/null |
|
||||
grep -Eq '^(ssh://)?([^@/]+@)?(node1(\.lab\.example)?|10\.99\.1\.11)(:[0-9]+)?[:/]+srv/git/playbooks\.git/?$'
|
||||
}
|
||||
# The other clones of it in greg's home.
|
||||
others() {
|
||||
find /home/greg -maxdepth 3 -name .git -type d 2>/dev/null | while read -r d; do
|
||||
d=${d%/.git}; [ "$d" = "$C" ] && continue
|
||||
origin_ok "$d" && printf '%s\n' "$d"
|
||||
done
|
||||
}
|
||||
log() { g -C "$1" log --oneline 2>/dev/null; }
|
||||
|
||||
task 1 "A bare repository on node1, cloned over SSH into /home/greg/playbooks, on main"
|
||||
need "$R on node1 is a bare repository" "[ \"\$(in_node node1 git --git-dir=$R rev-parse --is-bare-repository)\" = true ]"
|
||||
need "its default branch is main" "[ \"\$(in_node node1 git --git-dir=$R symbolic-ref HEAD)\" = refs/heads/main ]"
|
||||
need "$C is a Git clone" "[ -d $C/.git ]"
|
||||
need "its origin is devops@node1:$R (or another SSH URL for it)" "origin_ok $C"
|
||||
need "greg can reach origin over SSH from $C (git ls-remote origin)" "g -C $C ls-remote origin"
|
||||
need "$C is on the branch main" "[ \"\$(g -C $C symbolic-ref HEAD)\" = refs/heads/main ]"
|
||||
|
||||
task 2 "greg's Git name and email, set globally, on a pushed README.md"
|
||||
need "user.name is set in greg's global Git config" 'runuser -l greg -c "git config --global user.name" | grep -q .'
|
||||
need "user.email is set in greg's global Git config" 'runuser -l greg -c "git config --global user.email" | grep -q @'
|
||||
need "main in node1's repository has README.md" "in_node node1 git --git-dir=$R ls-tree --name-only main | grep -qx README.md"
|
||||
need "a commit on node1's main has greg's name and email as the author" 'in_node node1 git --git-dir=$R log --format="%an <%ae>" main | grep -qxF "$(runuser -l greg -c "git config --global user.name") <$(runuser -l greg -c "git config --global user.email")>"'
|
||||
|
||||
task 3 "A second clone of the repository, with the same log"
|
||||
need "another directory in greg's home is a clone of node1's $R" '[ -n "$(others)" ]'
|
||||
need "it has the same git log --oneline as $C, with a commit in it" '[ -n "$(log $C)" ] && for d in $(others); do [ "$(log "$d")" = "$(log $C)" ] && exit 0; done; exit 1'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="5.1 Cloning a repository"
|
||||
CHAPTER=/ex294/5-git/5-1-cloning/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,5 @@
|
||||
# The nodes (with the repository) go with nodes_down and greg (with his
|
||||
# clones and Git settings) with users_remove; the packages the lab
|
||||
# installed here.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,35 @@
|
||||
# The control node: ansible-core, git and greg's project, with his key.
|
||||
for p in ansible-core git-core; do pkg_installed $p || echo $p >>"$LAB_STATE/missing"; done
|
||||
pkg_installed ansible-core || pkg_install ansible-core
|
||||
pkg_installed git-core || pkg_install git-core
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'C'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
C
|
||||
printf '[nodes]\nnode1\nnode2\n' >/home/greg/ansible/inventory
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory
|
||||
|
||||
# The managed nodes: devops with greg's key and sudo, git, and no /srv/git.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'id devops >/dev/null 2>&1 || useradd -m devops
|
||||
echo devops:Lab-Pass-1 | chpasswd
|
||||
echo "devops ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/devops
|
||||
chmod 440 /etc/sudoers.d/devops
|
||||
install -d -m 700 -o devops -g devops /home/devops/.ssh
|
||||
cat >/home/devops/.ssh/authorized_keys
|
||||
chown devops: /home/devops/.ssh/authorized_keys
|
||||
chmod 600 /home/devops/.ssh/authorized_keys
|
||||
rm -rf /srv/git' </home/greg/.ssh/id_ed25519.pub
|
||||
say "${C_DIM}Installing git on $n...${C_0}"
|
||||
in_node "$n" 'rpm -q git-core >/dev/null || dnf -y -q install git-core >/dev/null'
|
||||
done
|
||||
@@ -0,0 +1,43 @@
|
||||
# EX294 5.2: Adding and pushing files
|
||||
|
||||
Add a playbook to a clone and push it, ignore files that don't belong in
|
||||
the repository, and get past a rejected push with a merge.
|
||||
|
||||
## Before you start
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key and a Git name and email, and installs
|
||||
`ansible-core` and `git`. Work as greg. His Ansible project is
|
||||
`/home/greg/ansible`, with a playbook `site.yml`.
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name. On node1, the bare
|
||||
repository `/srv/git/playbooks.git` (owned by `devops`, who has greg's
|
||||
key) has one commit, and greg's clone of it is `/home/greg/playbooks`,
|
||||
with origin `devops@node1:/srv/git/playbooks.git`. For a root shell on a
|
||||
node, `sudo podman exec -it node1 bash`. Everything stays on this VM: the
|
||||
repository lives on a node, not on a hosting service.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. Copy `/home/greg/ansible/site.yml` into `/home/greg/playbooks`, commit
|
||||
it and push it, so that a fresh clone in another directory contains
|
||||
it.
|
||||
2. In `/home/greg/playbooks`, add a `.gitignore` that ignores
|
||||
ansible-navigator's playbook artifacts (`*-artifact-*.json`) and log
|
||||
(`ansible-navigator.log`) and the vault password file `.vault_pass`,
|
||||
and commit it. Then `git status --short` doesn't list such files.
|
||||
3. Make a second clone of the repository, and push a commit from it. Then
|
||||
make a different commit in `/home/greg/playbooks` and try to push it:
|
||||
it's rejected. Pull with `git pull --no-rebase`, then push again, so
|
||||
that `git log --graph --oneline` shows the merge and node1's repository
|
||||
has it.
|
||||
|
||||
## Hints
|
||||
|
||||
- `git add`, `git commit -m`, `git push`; `git log origin/main --oneline`.
|
||||
- `touch site-artifact-2026-10-30T10:00:00.json ansible-navigator.log
|
||||
.vault_pass` and `git status --short` test the `.gitignore`.
|
||||
- Plain `git pull` on diverged branches stops with "Need to specify how
|
||||
to reconcile divergent branches"; `--no-rebase` merges, and
|
||||
`git config --global pull.rebase false` makes that the default.
|
||||
@@ -0,0 +1,37 @@
|
||||
nodes_wait 60 || true
|
||||
R=/srv/git/playbooks.git
|
||||
C=/home/greg/playbooks
|
||||
# g CMD: run git CMD as greg, over SSH with his key and no prompts.
|
||||
g() {
|
||||
setsid -w timeout 60 runuser -u greg -- env HOME=/home/greg \
|
||||
GIT_SSH_COMMAND="ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10" \
|
||||
git "$@" </dev/null
|
||||
}
|
||||
# rg CMD: git CMD in node1's repository.
|
||||
rg() { in_node node1 git --git-dir=$R "$@"; }
|
||||
# The other clones of it in greg's home.
|
||||
others() {
|
||||
find /home/greg -maxdepth 3 -name .git -type d 2>/dev/null | while read -r d; do
|
||||
d=${d%/.git}; [ "$d" = "$C" ] && continue
|
||||
g -C "$d" remote get-url origin 2>/dev/null |
|
||||
grep -Eq '^(ssh://)?([^@/]+@)?(node1(\.lab\.example)?|10\.99\.1\.11)(:[0-9]+)?[:/]+srv/git/playbooks\.git/?$' &&
|
||||
printf '%s\n' "$d"
|
||||
done
|
||||
}
|
||||
ignored() { g -C $C check-ignore -q --no-index "$1"; }
|
||||
|
||||
task 1 "site.yml committed and pushed"
|
||||
need "main in node1's repository has site.yml" "rg ls-tree -r --name-only main | grep -qx site.yml"
|
||||
need "it's the playbook from /home/greg/ansible" "rg show main:site.yml | grep -q 'name: A web server'"
|
||||
|
||||
task 2 "A committed .gitignore for artifacts, navigator logs and .vault_pass"
|
||||
need "$C/.gitignore is committed, with no changes since" "g -C $C ls-files --error-unmatch .gitignore && g -C $C diff --quiet HEAD -- .gitignore"
|
||||
need "it ignores navigator artifacts such as site-artifact-2026-10-30T10:00:00.json" "ignored site-artifact-2026-10-30T10:00:00.json"
|
||||
need "it ignores ansible-navigator.log" "ignored ansible-navigator.log"
|
||||
need "it ignores .vault_pass" "ignored .vault_pass"
|
||||
need "it doesn't ignore playbooks" "! ignored site.yml"
|
||||
|
||||
task 3 "A rejected push, a pull with a merge, and the push again"
|
||||
need "there's a second clone of node1's repository in greg's home" '[ -n "$(others)" ]'
|
||||
need "main in node1's repository has a merge commit" "rg rev-list --merges main | grep -q ."
|
||||
need "$C has pushed everything: its main is node1's main" "[ \"\$(g -C $C rev-parse main)\" = \"\$(rg rev-parse main)\" ]"
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="5.2 Adding and pushing files"
|
||||
CHAPTER=/ex294/5-git/5-2-adding-files/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,5 @@
|
||||
# The nodes (with the repository) go with nodes_down and greg (with his
|
||||
# clones and Git settings) with users_remove; the packages the lab
|
||||
# installed here.
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,64 @@
|
||||
# The control node: ansible-core, git and greg's project, with his key.
|
||||
for p in ansible-core git-core; do pkg_installed $p || echo $p >>"$LAB_STATE/missing"; done
|
||||
pkg_installed ansible-core || pkg_install ansible-core
|
||||
pkg_installed git-core || pkg_install git-core
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
install -d -m 755 -o greg -g greg /home/greg/ansible
|
||||
cat >/home/greg/ansible/ansible.cfg <<'C'
|
||||
[defaults]
|
||||
inventory = inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
C
|
||||
printf '[nodes]\nnode1\nnode2\n' >/home/greg/ansible/inventory
|
||||
cat >/home/greg/ansible/site.yml <<'C'
|
||||
---
|
||||
- name: A web server
|
||||
hosts: node1
|
||||
become: true
|
||||
tasks:
|
||||
- name: Install httpd
|
||||
ansible.builtin.dnf:
|
||||
name: httpd
|
||||
state: present
|
||||
|
||||
- name: Start and enable httpd
|
||||
ansible.builtin.service:
|
||||
name: httpd
|
||||
state: started
|
||||
enabled: true
|
||||
C
|
||||
chown greg: /home/greg/ansible/ansible.cfg /home/greg/ansible/inventory /home/greg/ansible/site.yml
|
||||
|
||||
# The managed nodes: devops with greg's key and sudo, and git.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node -i "$n" 'id devops >/dev/null 2>&1 || useradd -m devops
|
||||
echo devops:Lab-Pass-1 | chpasswd
|
||||
echo "devops ALL=(ALL) NOPASSWD: ALL" >/etc/sudoers.d/devops
|
||||
chmod 440 /etc/sudoers.d/devops
|
||||
install -d -m 700 -o devops -g devops /home/devops/.ssh
|
||||
cat >/home/devops/.ssh/authorized_keys
|
||||
chown devops: /home/devops/.ssh/authorized_keys
|
||||
chmod 600 /home/devops/.ssh/authorized_keys
|
||||
rm -rf /srv/git' </home/greg/.ssh/id_ed25519.pub
|
||||
say "${C_DIM}Installing git on $n...${C_0}"
|
||||
in_node "$n" 'rpm -q git-core >/dev/null || dnf -y -q install git-core >/dev/null'
|
||||
done
|
||||
|
||||
# The repository on node1 with one commit, and greg's clone of it.
|
||||
in_node node1 'install -d -o devops -g devops /srv/git/playbooks.git &&
|
||||
runuser -u devops -- git init -q --bare -b main /srv/git/playbooks.git'
|
||||
runuser -l greg -c 'ssh-keyscan -t ed25519 node1 2>/dev/null >>~/.ssh/known_hosts
|
||||
git config --global user.name "Greg"
|
||||
git config --global user.email [email protected]
|
||||
git -c init.defaultBranch=main clone -q devops@node1:/srv/git/playbooks.git 2>/dev/null
|
||||
cd playbooks && printf "# Playbooks\n\nAnsible work for the lab nodes.\n" >README.md &&
|
||||
git add README.md && git commit -q -m "Start the repository" && git push -q -u origin main' </dev/null
|
||||
[ "$(in_node node1 git --git-dir=/srv/git/playbooks.git rev-list --count main)" = 1 ] ||
|
||||
die "could not make the repository on node1"
|
||||
@@ -0,0 +1,52 @@
|
||||
# EX294 6.1: VS Code with Git and ansible-navigator
|
||||
|
||||
Clone a project in VS Code, commit and push a playbook from the Source
|
||||
Control view, and run playbooks from the Ansible extension's menu.
|
||||
|
||||
## Before you start
|
||||
|
||||
You need VS Code on your own computer with the Remote - SSH extension:
|
||||
connect to this VM as greg (with his password, or add your key to his
|
||||
`~/.ssh/authorized_keys`), and install Red Hat's Ansible extension
|
||||
(`redhat.ansible`) on the remote side when VS Code offers it.
|
||||
|
||||
This VM is the control node. The lab creates the user `greg` (password
|
||||
`Lab-Pass-1`) with an SSH key, and installs `ansible-core`, `git` and
|
||||
`ansible-navigator` (from PyPI, in `/opt/le-lab-navigator`). greg's Git
|
||||
repository is `/home/greg/git/ansible.git`, with one commit on `main`:
|
||||
an `ansible.cfg` that reads `inventory`, logs in as `devops` and becomes
|
||||
root, the inventory, and `site.yml`, which checks every node and adds a
|
||||
line to the node's `/var/log/lab-runs.log` saying which command ran it
|
||||
and the `--limit`. greg has no Git identity yet.
|
||||
|
||||
greg's podman has the execution environment image `localhost/lab-ee:2.16`
|
||||
(built on this VM from AlmaLinux 10's ansible-core and collections from
|
||||
Ansible Galaxy; no registry or login).
|
||||
|
||||
The managed nodes `node1` (10.99.1.11) and `node2` (10.99.1.12) are
|
||||
AlmaLinux 10 containers reached over SSH by name; on both, `devops`
|
||||
accepts greg's key and may use sudo without a password. For a root shell
|
||||
on a node, `sudo podman exec -it node1 bash`.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. In VS Code, clone `/home/greg/git/ansible.git`, create a new playbook in
|
||||
the Ansible language mode with completion, and commit and push it from
|
||||
the Source Control view. `git log origin/main --oneline` in the
|
||||
terminal shows the commit, and a fresh clone has the file.
|
||||
2. Turn on the extension's execution environment setting with the image
|
||||
`localhost/lab-ee:2.16`, and run a playbook from the right-click menu.
|
||||
Not graded: check that the terminal shows `ansible-navigator run ...
|
||||
--ee true --pae false --ce podman --eei localhost/lab-ee:2.16`.
|
||||
3. Set `ansible.playbook.arguments` to `--limit node1` and run `site.yml`
|
||||
via ansible-playbook from the menu. Only node1 appears in the recap.
|
||||
|
||||
## Hints
|
||||
|
||||
- Command Palette: **Git: Clone** takes the path as the URL. If the
|
||||
commit fails for want of `user.name`, set it with `git config --global`
|
||||
in the terminal.
|
||||
- The status bar shows the file's language mode; click **YAML** to
|
||||
choose **Ansible**.
|
||||
- Settings: search for `ansible.executionEnvironment` and
|
||||
`ansible.playbook.arguments` (Remote tab for this VM).
|
||||
@@ -0,0 +1,18 @@
|
||||
nodes_wait 60 || true
|
||||
OUT=$LAB_STATE/check.out
|
||||
R=/home/greg/git/ansible.git B=$(state_get base)
|
||||
# gg ARGS: git as greg in the repository.
|
||||
gg() { runuser -u greg -- git -C "$R" "$@" </dev/null; }
|
||||
for n in $(nodes_names); do in_node "$n" cat /var/log/lab-runs.log 2>/dev/null | sed "s/^/$n /"; done >"$OUT.runs"
|
||||
|
||||
task 1 "A new playbook committed and pushed to main"
|
||||
need "main has a commit after the lab's first one" '[ "$(gg rev-parse main)" != "$B" ] && gg merge-base --is-ancestor "$B" main'
|
||||
gg diff --name-only --diff-filter=A "$B" main 2>/dev/null | grep -E '\.ya?ml$' >"$OUT.new"
|
||||
need "the new commits add a .yml or .yaml file" 'grep -q . "$OUT.new"'
|
||||
need "a fresh clone has a new file that is a playbook (ansible-playbook --syntax-check)" 'C=$(runuser -u greg -- mktemp -d) && runuser -u greg -- git clone -q "$R" "$C/p" && ok=1; while read -r f; do timeout 60 runuser -l greg -c "cd $C/p && ansible-playbook --syntax-check $f" </dev/null && grep -q "hosts:" "$C/p/$f" && { ok=0; break; }; done <"$OUT.new"; rm -rf "$C"; exit $ok'
|
||||
|
||||
task 2 "A playbook run in the execution environment from the menu" self
|
||||
|
||||
task 3 "site.yml via ansible-playbook with --limit node1"
|
||||
need "node1 logged a run of site.yml by ansible-playbook with limit node1" 'grep -q "^node1 .* ansible-playbook, limit node1$" "$OUT.runs"'
|
||||
rm -f "$OUT".*
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="6.1 VS Code with Git and ansible-navigator"
|
||||
CHAPTER=/ex294/6-vs-code/6-1-vs-code-git-and-navigator/
|
||||
SYSTEMS="rhel"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Navigator, if the lab installed it. greg (with his project and his
|
||||
# podman images) goes with users_remove, the nodes with nodes_down.
|
||||
if [ "$(state_get navigator)" = installed ]; then
|
||||
rm -f /usr/local/bin/ansible-navigator
|
||||
rm -rf /opt/le-lab-navigator
|
||||
fi
|
||||
if id greg >/dev/null 2>&1; then
|
||||
runuser -l greg -c 'podman system reset -f' >/dev/null 2>&1
|
||||
fi
|
||||
while read -r p; do dnf -y -q remove "$p" >/dev/null 2>&1; done <"$LAB_STATE/missing" 2>/dev/null
|
||||
true
|
||||
@@ -0,0 +1,98 @@
|
||||
# The control node: ansible-core, git, python3-pip (for ansible-navigator's
|
||||
# virtual environment) and greg with a key and quiet rootless podman.
|
||||
for p in ansible-core python3-pip git; do
|
||||
pkg_installed $p || { echo $p >>"$LAB_STATE/missing"; pkg_install $p; }
|
||||
done
|
||||
user_add greg
|
||||
install -d -m 700 -o greg -g greg /home/greg/.ssh
|
||||
runuser -u greg -- ssh-keygen -q -t ed25519 -N '' -f /home/greg/.ssh/id_ed25519
|
||||
# Without a systemd user session (su, sudo -i), podman would warn on every
|
||||
# run; the cgroupfs manager works the same in any login.
|
||||
install -d -m 755 -o greg -g greg /home/greg/.config /home/greg/.config/containers
|
||||
printf '[engine]\ncgroup_manager = "cgroupfs"\nevents_logger = "file"\n' >/home/greg/.config/containers/containers.conf
|
||||
chown greg: /home/greg/.config/containers/containers.conf
|
||||
|
||||
# greg's repository: one commit with ansible.cfg, the inventory and
|
||||
# site.yml, which records each run on the node (task 3). greg has no Git
|
||||
# identity (task 1).
|
||||
d=$(mktemp -d)
|
||||
cat >"$d/ansible.cfg" <<'X'
|
||||
[defaults]
|
||||
inventory = ./inventory
|
||||
remote_user = devops
|
||||
host_key_checking = false
|
||||
|
||||
[privilege_escalation]
|
||||
become = true
|
||||
X
|
||||
printf '[nodes]\nnode1\nnode2\n' >"$d/inventory"
|
||||
cat >"$d/site.yml" <<'X'
|
||||
---
|
||||
- name: Check the managed nodes
|
||||
hosts: all
|
||||
vars:
|
||||
run_by: "{{ 'ansible-navigator' if lookup('ansible.builtin.env', 'ANSIBLE_STDOUT_CALLBACK') == 'awx_display' else 'ansible-playbook' }}"
|
||||
tasks:
|
||||
- name: Make sure the node answers
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Record this run in /var/log/lab-runs.log
|
||||
ansible.builtin.lineinfile:
|
||||
path: /var/log/lab-runs.log
|
||||
line: "{{ now(utc=true, fmt='%Y-%m-%d %H:%M:%S') }} {{ run_by }}, limit {{ ansible_limit | default('none') }}"
|
||||
create: true
|
||||
mode: "0644"
|
||||
changed_when: false
|
||||
X
|
||||
chown -R greg: "$d"
|
||||
install -d -m 755 -o greg -g greg /home/greg/git
|
||||
runuser -u greg -- git init -q --bare -b main /home/greg/git/ansible.git
|
||||
runuser -u greg -- bash -c "cd '$d' && git init -q -b main && git add . &&
|
||||
git -c user.name='Lab setup' -c [email protected] commit -q -m 'Start the project' &&
|
||||
git push -q /home/greg/git/ansible.git main"
|
||||
rm -rf "$d"
|
||||
state_set base "$(runuser -u greg -- git -C /home/greg/git/ansible.git rev-parse main)"
|
||||
|
||||
# The managed nodes, with devops: greg's key and passwordless sudo.
|
||||
nodes_up 2
|
||||
for n in $(nodes_names); do
|
||||
in_node "$n" 'useradd -m devops && printf "devops ALL=(ALL) NOPASSWD: ALL\n" >/etc/sudoers.d/devops && chmod 440 /etc/sudoers.d/devops && install -d -m 700 -o devops -g devops /home/devops/.ssh'
|
||||
in_node -i "$n" 'cat >/home/devops/.ssh/authorized_keys && chown devops: /home/devops/.ssh/authorized_keys && chmod 600 /home/devops/.ssh/authorized_keys' </home/greg/.ssh/id_ed25519.pub
|
||||
done
|
||||
|
||||
# ansible-navigator from PyPI, in its own virtual environment. Only the
|
||||
# command goes on the PATH, so ansible-playbook stays the RPM's (and
|
||||
# --ee false runs the control node's ansible-core).
|
||||
if [ ! -e /usr/local/bin/ansible-navigator ]; then
|
||||
say "${C_DIM}Installing ansible-navigator...${C_0}"
|
||||
python3 -m venv /opt/le-lab-navigator
|
||||
for i in 1 2 3; do
|
||||
/opt/le-lab-navigator/bin/pip install -q --disable-pip-version-check 'ansible-navigator==26.10.0' >/dev/null && break
|
||||
sleep 5
|
||||
done
|
||||
ln -s /opt/le-lab-navigator/bin/ansible-navigator /usr/local/bin/ansible-navigator
|
||||
/usr/local/bin/ansible-navigator --version >/dev/null
|
||||
state_set navigator installed
|
||||
fi
|
||||
|
||||
# The execution environment localhost/lab-ee:2.16: the node image with
|
||||
# ansible-core and ansible.posix and community.general from Ansible Galaxy.
|
||||
# Built as root, given to greg's rootless podman, and removed from root's.
|
||||
say "${C_DIM}Building the execution environment image...${C_0}"
|
||||
d=$(mktemp -d)
|
||||
cat >"$d/Containerfile" <<X
|
||||
FROM $LE_NODE_IMAGE
|
||||
LABEL ansible-execution-environment=true
|
||||
RUN dnf -y -q --setopt=install_weak_deps=False install ansible-core \\
|
||||
&& for i in 1 2 3; do \\
|
||||
ansible-galaxy collection install -p /usr/share/ansible/collections \\
|
||||
'ansible.posix:>=2,<3' 'community.general:>=11,<12' && break; sleep 5; \\
|
||||
done \\
|
||||
&& ansible-doc community.general.lvg >/dev/null && ansible-doc ansible.posix.seboolean >/dev/null \\
|
||||
&& dnf clean all
|
||||
X
|
||||
podman build -q --layers=false -t localhost/lab-ee:2.16 "$d" >/dev/null
|
||||
rm -rf "$d"
|
||||
podman save localhost/lab-ee:2.16 | runuser -l greg -c 'podman load -q' >/dev/null 2>&1
|
||||
podman rmi localhost/lab-ee:2.16 >/dev/null
|
||||
runuser -l greg -c 'podman image exists localhost/lab-ee:2.16'
|
||||
Loaded 100 of 190 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user