Chapter 1: Build Your Practice Lab

Troubleshooting and fast rebuild

Fixes for everything that went wrong while this lab was built and tested, a command to record your versions, and the whole build as a script for when you want to rebuild from scratch.

ufw on the host

Ubuntu's ufw firewall is off by default, but if you have enabled it, its default policy blocks three things the lab needs. It doesn't show up until the VMs first try to use the network.

SymptomWhat ufw is blocking
VMs show no IPv4 address in lxc listDHCP requests from the lab to the host (UDP 67)
Lab names like servera.lab.example.com don't resolveDNS requests from the lab to the host (port 53)
dnf or curl to the internet hangs inside VMsForwarding of lab traffic out to the internet
sudo ufw status                                             # "Status: active" means you need this
sudo ufw allow in on rhcebr0 to any port 67 proto udp       # DHCP: VMs get their IPs
sudo ufw allow in on rhcebr0 to any port 53                 # DNS: lab names resolve
sudo ufw route allow in on rhcebr0                          # forwarding: VMs reach the internet
sudo ufw reload
lxc restart --project rhce --all                            # VMs ask for their IPs again

These rules are scoped to rhcebr0, so nothing changes for the rest of the host. They don't open the lab to anyone either: ufw only adds permissions, and the rhce_isolate seal still drops traffic from the lab to your local network, and everything coming in.

sudo ufw status verbose | grep rhcebr0
lxc list --project rhce          # every VM has its 172.25.250.x address again

General problems

SymptomLikely causeFix
VM stuck at boot; Console shows a UEFI / secure boot errorSecure bootCheck security.secureboot: "false" in the profile, then restart the VM
lxc exec says the agent isn't runningFirst boot not finished, or not the cloud imageWait two minutes, or use the UI Console tab and log in as root/redhat. If still broken, recreate the VM from the cloud image.
Users don't existcloud-init didn't run or hit a YAML errorIn the VM: cloud-init status --long, less /var/log/cloud-init-output.log. Fix the profile and recreate the VM.
No IPv4 address in lxc listDHCP blocked, or NIC not on rhcebr0Check devices.eth0 in the VM's YAML. If ufw is active, see above.
"Instance DHCP static allocation … already used"Two VMs share an IPGive each a unique ipv4.address
Names like servera.lab.example.com don't resolveDNS domain missingCheck dns.domain: lab.example.com on rhcebr0, then restart the VMs
VMs can't reach the internetNAT off, or ufw/Docker blocking forwardingipv4.nat: "true". With ufw: see above. With Docker installed: sudo iptables -I DOCKER-USER -i rhcebr0 -j ACCEPT and sudo iptables -I DOCKER-USER -o rhcebr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
Need a VM to reach your local network temporarilyBlocked by designRemove 192.168.0.0/16 from the forward chain, then sudo systemctl restart rhce-isolate
Packages such as lvm2 or tar missing, getenforce says Disabled, yet cloud-init status says doneNo DNS or internet on the first boot (usually ufw); cloud-init only logs a warningFix the network, then repair the lab. grep -i "failed to download" /var/log/cloud-init-output.log confirms it
lxc exec: "LXD VM agent is not currently running" after switching SELinux onThe agent's label rule is missing, so SELinux blocks it (the console log shows denied { create } … vsock_socket)Log in from workstation (ssh devops@servera) and run the three SELinux lines from the repair steps, or rht-vmctl reset the VM
Same, straight after an SELinux exerciseA playbook purged local SELinux changes (for example selinux_fcontexts_purge: true in the SELinux system role)rht-vmctl reset the VM; in your own playbooks, leave purging off
"couldn't resolve module/action 'ansible.posix.firewalld'"Collections not installed, or collections_path in ansible.cfg lists only ./collectionsInstall them as in section 1.6, step 2, and use the three-path line from section 1.8
"Collection … does not support Ansible version 2.14"An unpinned ansible-galaxy install picked the newest releasePin ansible.posix:1.5.4 and community.general:9.5.13
lab: command not found, or lab start says there is no such exerciseThe lab command is not installed, or workstation has no internetSection 1.6, step 7. lab list shows the exercise names
A playbook with remote_user: root (or none) fails with Permission denied (publickey…)The SSH key is only installed for devopsSection 1.6, step 6. lab check shows which logins are missing
Those logins worked, then stopped after rht-vmctl reset serversThe clean baseline was saved before the keys were set up, so every reset removes themRepeat steps 5 and 6 of section 1.6, confirm with lab check, then run rht-vmctl save on the host
lab finish did not reset the serversBy design: lab runs inside the sealed lab network and cannot reach the hostRun rht-vmctl reset servers on the Ubuntu host
curl http://servera from workstation fails, but the web server is runningfirewalld on the server, as in the classroomOpen the port in your playbook with ansible.posix.firewalld (service: http, permanent: true, immediate: true)
lsblk doesn't show the 5 GiB diskVolume made as filesystem, not blockRecreate it with Content type: block; attach it with the VM stopped
VM-to-VM SSH: "Permission denied (publickey)"Password login disabledCheck ssh_pwauth: true in the profile, or on the VM: sudo sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config.d/*.conf && sudo systemctl restart sshd
Warning: "is using the discovered Python interpreter at /usr/bin/python3.9"Harmless; Ansible found Python by searchingAdd interpreter_python = auto_silent to [defaults] in ansible.cfg
Every host shows yellow CHANGED for ansible … -m commandNormal for command/shellNothing to fix; those modules always report changed
Restore fails: "cannot be restored due to subsequent snapshot(s)"ZFS only rolls back to the newest snapshotSee the ZFS snapshot rule. Restore the newest snapshot, or delete newer ones with rht-vmctl rmsnap
After rht-vmctl reset, sdb still has partitions or a volume groupThe extra disk has no clean snapshot: an older rht-vmctl only snapshotted the VMInstall the current script from section 1.7, clear the disk once (vgremove, wipefs -a /dev/sdb1 /dev/sdb), then rht-vmctl save. reset warns when a disk has no snapshot
rht-vmctl reset says there's no clean snapshotBaseline never takenrht-vmctl snaps to check, then rht-vmctl save
Snapshot times look hours offLXD shows them in UTCConvert from UTC to your local time zone
ansible --version shows core 2.19 or newerA pip-installed ansible-core is ahead of Rocky's on PATHSee the warning in section 1.6, step 3
which -a ansible lists the same path several timesA PATH line was appended to ~/.bashrc more than onceRocky's default ~/.bashrc already adds ~/.local/bin; delete the extra export PATH=… lines
Navigator: "Additional properties are not allowed ('enable' was unexpected)"execution-environment needs enabled, not enableRecreate the file with the command in section 1.6, step 4, then rm -f ~/ansible-navigator.log
ansible-navigator: command not foundSymlink missing, or a new shell not startedRe-run the ln -sf line from section 1.6 step 3, then exec bash -l

Repair an existing lab

For a lab whose first boot missed its packages, or one built before the profile in section 1.4 switched on SELinux, firewalld and chrony, or added the manual pages and everyday tools the system administration path uses. It brings running VMs up to date in place, keeps everything on them, and takes about five minutes.

for vm in workstation servera serverb serverc serverd utility; do
  lxc info --project rhce "$vm" >/dev/null 2>&1 || continue
  lxc exec --project rhce "$vm" -- bash -c '
    dnf install -y python3 python3-libselinux vim-enhanced bash-completion lvm2 tar rsync \
      man-db man-pages tree tmux lsof nano bind-utils \
      selinux-policy-targeted policycoreutils-python-utils firewalld chrony
    semanage fcontext -a -t bin_t "/var/run/lxd_agent(/.*)?" 2>/dev/null || true
    systemctl enable --now firewalld chronyd' </dev/null
done
lxc exec --project rhce utility -- bash -c 'firewall-cmd --permanent --add-service=http && firewall-cmd --reload' 2>/dev/null
lxc restart --project rhce --all      # each VM boots twice while SELinux labels its disk

Then install the current rht-vmctl from section 1.7 (it also snapshots the extra disks), and on workstation run steps 1, 2, 5, 6 and 7 of section 1.6 (packages, collections, SSH keys for devops, student and root, and the lab command), and if utility serves collections, download the pinned versions there too. After about two minutes, run the verification in section 1.7. When every line passes, replace the baseline with rht-vmctl save: the old clean doesn't have these fixes.

Record your versions

Handy when comparing notes or filing an issue. Run it on the host once the lab is built:

echo "== host";  grep PRETTY /etc/os-release; uname -r; snap list lxd; lxc version
echo "== image"; lxc config get --project rhce servera image.description
for vm in workstation servera; do
  echo "== $vm"; lxc exec --project rhce "$vm" -- bash -c 'cat /etc/rocky-release; getenforce; rpm -q selinux-policy-targeted'
done
echo "== workstation tools"
lxc exec --project rhce workstation -- su - student -c '
  which -a ansible; ansible --version | head -1
  rpm -q ansible-core rhel-system-roles podman python3.11
  ansible-navigator --version
  ansible-galaxy collection list 2>/dev/null | grep -E "^(ansible|community|redhat)\."'

Rebuild everything fast

Everything in this chapter as commands, once you know what each step does. It assumes the host is prepared and the seal is enabled (sections 1.2 and 1.3, plus the ufw rules if ufw is active), and that the profile YAML from section 1.4 is saved as rhce-profile.yaml on the host. Afterwards, continue with sections 1.6 and 1.7 as normal: prepare workstation (including the collections), add utility if you want it, run the verification, then save clean.

Download the script. Read it before running it. The expandable example below matches the download.

#!/usr/bin/env bash
set -euo pipefail

# Network (Phase 03)
lxc network create rhcebr0 --project default \
  ipv4.address=172.25.250.254/24 ipv4.nat=true \
  ipv4.dhcp.ranges=172.25.250.100-172.25.250.199 \
  ipv6.address=none dns.domain=lab.example.com
lxc network set rhcebr0 --project default \
  raw.dnsmasq="$(printf 'host-record=content.example.com,172.25.250.8\nhost-record=materials.example.com,172.25.250.8')"

# Project (Phase 05)
lxc project create rhce -c features.images=false -c features.profiles=true \
  -c features.storage.volumes=true -c features.networks=false
lxc project switch rhce

# Profile (Phase 06)
lxc profile edit default < rhce-profile.yaml

# Disks and VMs (Phases 07–08)
declare -A IP=( [workstation]=9 [servera]=10 [serverb]=11 [serverc]=12 [serverd]=13 )
for vm in workstation servera serverb serverc serverd; do
  lxc init "${IMAGE:-images:rockylinux/9/cloud}" "$vm" --vm    # IMAGE=rocky9-lab to use a frozen copy
  lxc config device override "$vm" eth0 ipv4.address=172.25.250.${IP[$vm]}
  if [[ $vm == server* ]]; then
    lxc storage volume create default "$vm-disk2" --type=block size=5GiB
    lxc config device add "$vm" disk2 disk pool=default source="$vm-disk2"
  fi
done
lxc config set workstation limits.cpu=2 limits.memory=2GiB

sudo systemctl restart rhce-isolate     # make sure the seal is on before first boot
lxc start --all
sleep 240                                # first boot + the SELinux relabel reboot
lxc list

Tear the whole lab down

The same commands remove the lighter system administration lab, which has only workstation, servera and serverb.

lxc project switch rhce
for vm in workstation servera serverb serverc serverd utility; do
  lxc delete --force "$vm" 2>/dev/null || true
done
for v in servera serverb serverc serverd; do
  lxc storage volume delete default "$v-disk2" 2>/dev/null || true
done
lxc project switch default
lxc project delete rhce
lxc network delete rhcebr0
sudo systemctl disable --now rhce-isolate