8.8 KiB
Writing a lab
A lab is one study guide chapter's "Try it" section turned into a scenario
that le-lab can set up, grade and undo. Copy
labs/lfcs/5-1-user-group-accounts/ as a starting point.
Files
labs/<guide>/<chapter-slug>/
lab.conf title, chapter link, supported systems
README.md what the lab is, what it sets up, the numbered tasks
setup.sh prepares the scenario
check.sh grades it
reset.sh undoes it
The folder name is the chapter's file name, so lfcs/2-4-openssh is the lab
for /lfcs/2-networking/2-4-openssh/.
lab.conf is shell variables:
TITLE="2.4 OpenSSH"
CHAPTER=/lfcs/2-networking/2-4-openssh/
SYSTEMS="rhel debian" # or just one of them
README.md has a short intro, ## Before you start (what setup made:
users, disks, the peer), ## Tasks as a numbered list, and ## Hints.
le-lab tasks prints the Tasks section, so keep it readable as plain text.
Task numbers in the README and in check.sh must match.
The scripts
All three run as root, with lib/lab.sh already sourced. setup.sh runs
with set -e. Read lib/lab.sh for the helpers; the ones most labs use:
is_rhel,is_debian,pick RHEL-VALUE DEBIAN-VALUEpkg_install nameorpkg_install rhel-name/debian-nameuser_add NAME [useradd options]: a lab user with passwordLab-Pass-1, removed again by resetdisk_add N SIZE: spare disk N as/dev/loop4N, attached again at every boot; reset removes it and anything on itpeer_up,in_peer CMD: a second machine as a network namespace, at10.99.0.2(namepeer), linked to the VM's10.99.0.1nodes_up N,in_node NAME CMD: managed nodesnode1..nodeNfor Ansible labs (see "Managed nodes" below)hosts_add LINE: an/etc/hostsline reset takes out againstate_set KEY VALUE,state_get KEY: notes for check and resetsshd_restart: restart sshd after reset puts its settings back, and check port 22 answers (on Ubuntu, never restartssh.serviceyourself)
The peer shares the VM's files, users and /etc, but has its own
addresses, routes, firewall and ports. It has no route beyond the VM, so
DNS lookups made there time out: give peer services addresses, not names,
and bind them to 10.99.0.2 (which resolves from /etc/hosts), for
example python3 -m http.server 8080 --bind 10.99.0.2. A service that
should run in the peer gets NetworkNamespacePath=/run/netns/lepeer and
After=le-lab-peer.service in its unit; the peer comes back at every
boot.
Managed nodes
Where a guide has a control node and managed nodes (EX294), the VM is the
control node and nodes_up N (1 to 9) in setup.sh adds node1..nodeN:
podman containers from AlmaLinux 10 (quay.io/almalinuxorg/10-init, no
login needed), at 10.99.1.11, .12, ... with names in /etc/hosts.
Each has systemd as PID 1, its own files, packages, users, services and
cron, and sshd on port 22. root and ansible (in wheel, so sudo asks
for the password) have password Lab-Pass-1, and root may log in with it.
dnf uses the real AlmaLinux repositories; the node image starts with a
copy of the VM's dnf metadata, so the first dnf in a node is quick.
The image is about 200 MB to pull (seconds from quay.io) and 300 MB
built; nodes_up takes about 20 seconds once podman is installed. In the
nodes, dnf moves on from a mirror slower than 100 kB/s after 15 seconds.
Tested on the RHEL family; EX294 labs are SYSTEMS="rhel".
in_node NAME CMD...runs a command as root in a node (one argument runs as shell code);in_node -i NAME 'cat >FILE' <<EOFpasses standard input. Use it in check.sh to grade a node:need "devops exists on node1" 'in_node node1 id devops'.nodes_nameslists the nodes,node_addr NAMEprints an address,nodes_waitwaits until every node's sshd answers (check.sh can start withnodes_wait 60 || true).- For a root shell on a node, students use
sudo podman exec -it node1 bash. - Reset calls
nodes_downfor you: it removes the containers, their network,le-lab-nodes.service, the images setup pulled or built, the/etc/hostslines, and podman itself (with all its containers and images) ifnodes_upinstalled it.
The containers are kept, not recreated, so whatever a student does in a
node survives a reboot of the VM: le-lab-nodes.service starts them again
at boot and waits for their sshd before multi-user.target.
systemctl reboot inside a node restarts just that container, also with
its state kept.
What works in a node: packages (dnf), users, groups, passwords and
sudo, files and templates, services and targets (systemctl enable,
mask, set-default), cron, the journal, and firewalld once it's
installed (each node has its own network stack, so a node's firewall
really blocks the control node). The image has only sshd, sudo, Python,
cron and a few tools; a lab installs anything else it needs with
in_node NAME dnf -y -q install ....
What doesn't, because the nodes share the VM's kernel:
- SELinux is off inside a node, so
sefcontext,seboolean,selinuxandrestorecontasks have nothing to act on. - Storage: a node has no block devices and may not mount; partitions, LVM, file systems and fstab mounts fail there.
- Time:
chronydis skipped (no permission to set the clock), so it can be enabled but never active. - Rebooting: the boot ID and uptime are the VM's, so Ansible's
rebootmodule waits in vain; reboot the VM instead. - Host names:
hostnamectlandhostnameare refused (podman sets each node's name), and kernel settings (sysctl, boot loader, kernel modules) belong to the VM.
Tasks of those kinds target the control node itself: put localhost ansible_connection=local in the inventory (a group such as [storage]),
give the VM spare disks with disk_add, and grade the VM as usual.
setup.sh makes the starting point. Every graded task must fail right
after setup: if a fresh system already passes a task, set up the opposite.
Install what the scenario needs, but never what a task asks the student to
install. If the VM doesn't meet a precondition, stop with refuse MESSAGE
before changing anything: the lab isn't marked as started, so the student
can fix it and run start again.
check.sh lists the tasks and what must be true for each:
task 1 "Key-only logins, no root, only group sshusers"
need "sshd allows no password logins" 'sshd -T | grep -qx "passwordauthentication no"'
need "sshd is running" active sshd
task 2 "Make sshd listen on 2222 too" rhel # graded on RHEL relatives only
task 3 "Reset root's password from the boot loader" self # not graded
A check in one quoted string runs as shell code, so it can use pipes, !
and $(...). The first failing need is shown to the student, so word it
as what should be true. Check results, not methods: any correct approach
must pass. Where a task asks for a finding ("which process..."), have the
student write it to /root/lab-answers/<task> and compare it with answer N or grep.
Tasks that say "after a reboot" are graded on what makes them persistent (the config file, the enabled unit) and on the live state.
Give servers time to start: a check that grades a port the student opened
should poll for a few seconds (up to about 15) before failing. python3 -m http.server in particular looks up its bind address in DNS before it
listens, which can take over 10 seconds unless the address is in
/etc/hosts; for lab-made web servers, use a small script on
http.server.ThreadingHTTPServer with server_bind kept to the plain
socket bind instead.
Mark a task self only when it can't be checked from the running system,
such as a boot-loader recovery. Say in its README text how to check it.
Logs need care. AlmaLinux keeps the journal only in memory until someone
makes it persistent, so a message from before a reboot is gone after it;
read /var/log/messages or /var/log/secure as well, or mark the lab
noreboot. rsyslog writes logger messages to those files a moment
later, so a check that runs right away can miss them: poll briefly.
reset.sh undoes the scenario and the usual results of the tasks (the
users, files, units and settings they create), so that le-lab start on the
same lab scores 0% again. That includes packages a task installs: note in
setup which ones were missing, and remove them again in reset. Lab users,
spare disks and the peer are removed for you after reset.sh runs. Don't
fail on things that aren't there.
Rules
- Tasks come from the chapter's "Try it" section. Never use exam questions or tasks from anywhere else.
- Labs run on one VM. Where the guide says "the other VM", use the peer;
where it says "a spare disk", use
disk_add. - Never touch the interface or address the student is connected over, the root filesystem, or the boot loader, unless the task is about exactly that.
- Plain words, US spelling, no product names the guide doesn't use.