Practice labs for RHCSA, LPIC-1 and Linux+ #2

Merged
jcoffey-dev merged 4 commits from labs/batch-1 into main 2026-10-30 22:53:05 +00:00
781 changed files with 13753 additions and 2 deletions

No files matched your search

+10 -2
View File
@@ -95,10 +95,18 @@ 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. Lab users, spare disks and the peer are removed
for you after `reset.sh` runs. Don't fail on things that aren't there.
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
@@ -0,0 +1,39 @@
# Linux+ 1.1.1: The boot process
How the system booted, a trip into rescue mode, and a lasting change to the
kernel command line.
## Before you start
Take a snapshot first: task 2 and task 3 go through the boot loader. On the
RHEL family the lab adds `quiet` to every kernel's command line if it isn't
there, so that task 3 has something to remove. On Ubuntu it makes sure
`loglevel=7` isn't set. Reset puts the boot loader settings back as they
were.
## Tasks
1. Find out how this system booted. Write `UEFI` or `BIOS` on the first
line of `/root/lab-answers/1`, and the default systemd target (such as
`multi-user.target`) on the second.
2. Boot once into rescue mode from the boot loader menu, log in as root,
run `systemctl list-units --state=running`, then `systemctl reboot`. Not
graded: check yourself that only a handful of units were running. Root
needs a password for rescue mode (`passwd root`); lock it again
afterwards with `passwd -l root`.
3. Make a lasting change to the kernel command line, then reboot. RHEL
family: remove `quiet` from every installed kernel with grubby. Ubuntu:
add `loglevel=7` to `GRUB_CMDLINE_LINUX_DEFAULT` and run `update-grub`.
Check `/proc/cmdline` after the reboot. Leave the change in place until
you've run `le-lab check`; reset puts it back.
## Hints
- `/sys/firmware/efi` exists only on UEFI systems; `systemctl get-default`.
- In the GRUB menu, **e** edits an entry: add `systemd.unit=rescue.target`
to the end of the `linux` line and press **Ctrl+x**. Press **Esc** or hold
**Shift** if the menu is hidden.
- `grubby --update-kernel=ALL --remove-args=...`, `grubby --info=ALL`.
- On Ubuntu, files in `/etc/default/grub.d/` are read after
`/etc/default/grub` and can set the same variable; cloud images set
`GRUB_CMDLINE_LINUX_DEFAULT` in `50-cloudimg-settings.cfg`.
@@ -0,0 +1,21 @@
line() { sed -n "${1}p" /root/lab-answers/1 2>/dev/null | tr 'A-Z' 'a-z' | tr -d '[:space:]'; }
fw=bios; [ -d /sys/firmware/efi ] && fw=uefi
task 1 "UEFI or BIOS, and the default target, in /root/lab-answers/1"
need "the first line says ${fw^^}" '[ "$(line 1)" = "$fw" ]'
need "the second line is the default target, $(systemctl get-default)" '[ "$(line 2)" = "$(systemctl get-default)" ]'
task 2 "Boot once into rescue mode" self
if is_rhel; then
task 3 "quiet removed from every kernel's command line" rhel
need "no boot entry has quiet (grubby --info=ALL)" '[ -n "$(grubby --info=ALL | grep "^args=")" ] && ! grubby --info=ALL | grep "^args=" | grep -Eq "[\" ]quiet[\" ]"'
need "the running kernel was booted without quiet (reboot)" '! grep -Eq "(^| )quiet( |$)" /proc/cmdline'
else
task 3 "loglevel=7 added to GRUB_CMDLINE_LINUX_DEFAULT" debian
need "GRUB_CMDLINE_LINUX_DEFAULT ends up with loglevel=7" '
v=$(bash -c ". /etc/default/grub; for f in /etc/default/grub.d/*.cfg; do [ -f \"\$f\" ] && . \"\$f\"; done; printf %s \"\$GRUB_CMDLINE_LINUX_DEFAULT\"")
grep -Eq "(^| )loglevel=7( |$)" <<<"$v"'
need "/boot/grub/grub.cfg has loglevel=7 (update-grub)" 'awk "\$1 == \"linux\"" /boot/grub/grub.cfg | grep -Eq "[[:space:]]loglevel=7([[:space:]]|$)"'
need "the running kernel was booted with loglevel=7 (reboot)" 'grep -Eq "(^| )loglevel=7( |$)" /proc/cmdline'
fi
@@ -0,0 +1,3 @@
TITLE="1.1.1 The boot process"
CHAPTER=/linux-plus/1-system-management/1-1-1-boot-process/
SYSTEMS="rhel debian"
@@ -0,0 +1,12 @@
if is_rhel; then
if [ -n "$(state_get added-quiet)" ]; then
grubby --update-kernel=ALL --remove-args=quiet 2>/dev/null
elif [ -d "$LAB_STATE" ]; then
grubby --update-kernel=ALL --args=quiet 2>/dev/null
fi
elif [ -f "$LAB_STATE/grub.tar" ]; then
rm -rf /etc/default/grub.d
tar -C /etc/default -xf "$LAB_STATE/grub.tar"
update-grub >/dev/null 2>&1
fi
true
@@ -0,0 +1,16 @@
if is_rhel; then
command -v grubby >/dev/null || refuse "grubby isn't installed; this lab needs it (dnf install grubby)"
# Something for task 3 to remove: quiet on every kernel.
if grubby --info=ALL | grep '^args=' | grep -Evq '[" ]quiet[" ]'; then
grubby --update-kernel=ALL --args=quiet
state_set added-quiet 1
fi
else
command -v update-grub >/dev/null || refuse "update-grub isn't installed; this lab needs GRUB"
tar -C /etc/default -cf "$LAB_STATE/grub.tar" grub $(cd /etc/default && ls -d grub.d 2>/dev/null)
# Task 3 adds loglevel=7, so it mustn't be there yet.
if grep -qs 'loglevel=7' /etc/default/grub /etc/default/grub.d/*.cfg; then
sed -i 's/ *loglevel=7//' /etc/default/grub /etc/default/grub.d/*.cfg
update-grub >/dev/null 2>&1
fi
fi
@@ -0,0 +1,30 @@
# Linux+ 1.1.2: The filesystem hierarchy
A tour of the directory tree: links into `/usr`, what uses space under
`/var`, real and virtual filesystems, and which package owns a file.
## Before you start
The lab sets nothing up. Each task asks for a finding: write it to the file
in `/root/lab-answers/` that the task names.
## Tasks
1. Look at `/bin`, `/sbin`, `/lib` and `/tmp` with `ls -ld`. Write the ones
that are symbolic links, one per line, to `/root/lab-answers/1`.
2. Find the directory under `/var` that uses the most space, and write its
full path (such as `/var/spool`) to `/root/lab-answers/2`.
3. List the mount points of the real filesystems (not virtual ones such as
`/proc`), one per line, in `/root/lab-answers/3`.
4. Ask the package manager which package owns `/usr/bin/ls`, and write the
package name to `/root/lab-answers/4`. Then create `/etc/mynote` and see
what it says about that file; remove `/etc/mynote` again.
## Hints
- `ls -ld /bin /sbin /lib /tmp`: links start with `l`. Note the `t` at the
end of `/tmp`'s permissions: the sticky bit.
- `du -sh /var/* 2>/dev/null | sort -h | tail`.
- `findmnt --real`, `df -hT`; `findmnt --real -rn -o TARGET` prints just
the mount points.
- `rpm -qf` on the RHEL family, `dpkg -S` on Debian and Ubuntu.
@@ -0,0 +1,26 @@
# Answers as plain lines, lowercase, without spaces or a trailing slash.
lines() { tr 'A-Z' 'a-z' <"/root/lab-answers/$1" 2>/dev/null | tr -d ' \t\r' | sed 's|\(.\)/*$|\1|' | grep .; }
links=$(for d in /bin /sbin /lib /tmp; do [ -L "$d" ] && echo "$d"; done)
# The two biggest directories under /var: the answer may change a little.
biggest=$(du -s /var/* 2>/dev/null | sort -n | tail -n2 | awk '{print $2}')
real=$(findmnt --real -rn -o TARGET)
if is_rhel; then owner=$(rpm -qf --qf '%{NAME}\n' /usr/bin/ls | head -n1); else owner=$(dpkg -S /usr/bin/ls | head -n1 | cut -d: -f1); fi
task 1 "The symbolic links among /bin /sbin /lib /tmp"
need "/root/lab-answers/1 lists the links: $(echo $links)" '
[ "$(lines 1 | sed "s|^\([^/]\)|/\1|" | sort -u)" = "$(sort -u <<<"$links")" ]'
task 2 "The biggest directory under /var"
need "/root/lab-answers/2 names the biggest directory under /var" '
a=$(lines 2 | head -n1); a=/var/${a#/var/}; grep -qxF "$a" <<<"$biggest"'
task 3 "The mount points of the real filesystems"
need "/root/lab-answers/3 lists every real filesystem's mount point" '
[ -n "$(lines 3)" ] && while read -r m; do lines 3 | grep -qxF "$m" || exit 1; done <<<"$real"'
need "/root/lab-answers/3 lists no virtual filesystems" '
lines 3 | while read -r m; do grep -qxF "$m" <<<"$real" || exit 1; done'
task 4 "The package that owns /usr/bin/ls"
need "/root/lab-answers/4 names the package $owner" '
a=$(lines 4 | head -n1); a=${a%%:*}; [ "$a" = "$owner" ] || [[ $a == "$owner"-[0-9]* ]] || [[ $a == "$owner"_[0-9]* ]]'
need "/etc/mynote is removed again" '[ ! -e /etc/mynote ]'
@@ -0,0 +1,3 @@
TITLE="1.1.2 The filesystem hierarchy"
CHAPTER=/linux-plus/1-system-management/1-1-2-filesystem-hierarchy/
SYSTEMS="rhel debian"
@@ -0,0 +1,2 @@
rm -f /etc/mynote
true
@@ -0,0 +1,2 @@
# Nothing to set up: the tasks look at the system as it is.
rm -f /etc/mynote
@@ -0,0 +1,28 @@
# Linux+ 1.1.3: Architectures, distributions and licenses
Finding the machine architecture and the distribution family, a package's
license, and a package that isn't tied to one architecture.
## Before you start
The lab sets nothing up. Each task asks for a finding: write it to the file
in `/root/lab-answers/` that the task names.
## Tasks
1. Write the machine architecture on the first line of
`/root/lab-answers/1`, and the distribution family this system says it
belongs to (one of the words in `ID_LIKE`, such as `rhel` or `debian`) on
the second.
2. Find the license of the `bash` package and write it to
`/root/lab-answers/2`.
3. Find a package that isn't architecture-specific and write its name to
`/root/lab-answers/3`.
## Hints
- `uname -m`; `grep -E '^(ID|ID_LIKE|VERSION_ID)=' /etc/os-release`.
- `rpm -q --qf '%{NAME}: %{LICENSE}\n' bash` on the RHEL family;
`/usr/share/doc/bash/copyright` on Debian and Ubuntu.
- `rpm -qa --qf '%{ARCH} %{NAME}\n' | grep ^noarch`;
`dpkg -l | awk '$4=="all"{print $2}'`.
@@ -0,0 +1,17 @@
line() { sed -n "${2}p" "/root/lab-answers/$1" 2>/dev/null | tr 'A-Z' 'a-z' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'; }
family=$(. /etc/os-release; echo "${ID_LIKE:-$ID}" | tr 'A-Z' 'a-z')
task 1 "The architecture and the distribution family"
need "the first line is the architecture, $(uname -m)" '[ "$(line 1 1)" = "$(uname -m)" ]'
need "the second line is a word from ID_LIKE ($family)" 'a=$(line 1 2 | tr -d "\""); [ -n "$a" ] && grep -qw -- "$a" <<<"$family"'
task 2 "The license of bash"
need "/root/lab-answers/2 names bash's license (a version of the GPL)" 'line 2 1 | grep -Eq "gpl.*3|gnu general public license.*3"'
task 3 "A package that isn't architecture-specific"
need "/root/lab-answers/3 names an installed package" '[ -n "$(line 3 1)" ]'
if is_rhel; then
need "that package's architecture is noarch" '[ "$(rpm -q --qf "%{ARCH}" "$(line 3 1)" 2>/dev/null)" = noarch ]'
else
need "that package's architecture is all" '[ "$(dpkg-query -W -f "\${Architecture}" "$(line 3 1)" 2>/dev/null)" = all ]'
fi
@@ -0,0 +1,3 @@
TITLE="1.1.3 Architectures, distributions and licenses"
CHAPTER=/linux-plus/1-system-management/1-1-3-architectures-distributions-licensing/
SYSTEMS="rhel debian"
@@ -0,0 +1 @@
true
@@ -0,0 +1,2 @@
# Nothing to set up: the tasks look at the system as it is.
true
@@ -0,0 +1,37 @@
# Linux+ 1.2.1: Kernel modules and the initrd
Looking at modules, loading one with an option, loading it at every boot,
and finding the root disk's driver in the initrd.
## Before you start
The lab unloads the `dummy` module, a harmless one that makes network
devices. Reboot when task 3 says so; the check looks at the running system.
## Tasks
1. Find the license of the `xfs` module and write it to
`/root/lab-answers/1`.
2. Load `dummy` with the option `numdummies=3`, check that it made three
devices, and unload it again. Not graded: `ip -br link show type dummy |
wc -l` should print `3` before you unload it.
3. Make `dummy` load at every boot with the option `numdummies=1`, then
reboot. After the reboot, `ip -br link show type dummy` shows exactly one
device. Keep your files until you've run `le-lab check`; reset removes
them.
4. Look inside your initrd, and find the driver for your root disk
(`virtio_blk`, `nvme` or `ahci`, for example). Write its name to
`/root/lab-answers/4`.
## Hints
- `lsmod`, `modinfo xfs`, `modinfo -F license`.
- `modprobe dummy numdummies=3`, `modprobe -r dummy`.
- `/etc/modules-load.d/` for loading, `/etc/modprobe.d/` for options. Files
in every `modprobe.d` directory are read in name order and the last
setting wins; `modprobe --show-depends dummy` shows what modprobe will
pass.
- `lsinitrd` on the RHEL family, `lsinitramfs /boot/initrd.img-$(uname -r)`
on Ubuntu, which builds `virtio_blk` into the kernel:
`grep virtio_blk /lib/modules/$(uname -r)/modules.builtin`. `lsblk` shows
the root disk.
@@ -0,0 +1,26 @@
# Every driver between the root filesystem's disk and the bus it hangs off.
root_drivers() {
local src disk p
src=$(findmnt -n -o SOURCE / | sed 's/\[.*//')
disk=$(lsblk -nspo NAME "$src" | tail -n1)
p=$(readlink -f "/sys/class/block/${disk##*/}/device")
while [ -n "$p" ] && [ "$p" != /sys/devices ] && [ "$p" != / ]; do
[ -e "$p/driver" ] && basename "$(readlink -f "$p/driver")"
p=${p%/*}
done
}
task 1 "The license of the xfs module"
need "/root/lab-answers/1 has xfs's license, $(modinfo -F license xfs 2>/dev/null)" '[ "$(answer 1)" = "$(modinfo -F license xfs | tr A-Z a-z)" ]'
task 2 "dummy loaded with numdummies=3, then unloaded" self
task 3 "dummy loads at every boot with one device"
need "dummy is set to load at boot" '
cat /etc/modules-load.d/*.conf /run/modules-load.d/*.conf /usr/lib/modules-load.d/*.conf /etc/modules 2>/dev/null | grep -Eq "^[[:space:]]*dummy[[:space:]]*$"'
need "modprobe gives dummy numdummies=1 (the last setting wins)" 'modprobe --show-depends dummy 2>/dev/null | grep -o "numdummies=[0-9]*" | tail -n1 | grep -qx numdummies=1'
need "the dummy module is loaded" '[ -d /sys/module/dummy ]'
need "ip -br link show type dummy shows one device" '[ "$(ip -br link show type dummy | wc -l)" = 1 ]'
task 4 "The root disk's driver"
need "/root/lab-answers/4 names a driver of the root disk ($(root_drivers | head -n1))" '[ -n "$(answer 4)" ] && root_drivers | grep -qxF "$(answer 4 | tr - _)"'
@@ -0,0 +1,3 @@
TITLE="1.2.1 Kernel modules and the initrd"
CHAPTER=/linux-plus/1-system-management/1-2-1-kernel-modules-and-initrd/
SYSTEMS="rhel debian"
@@ -0,0 +1,8 @@
for f in /etc/modprobe.d/*.conf /etc/modules-load.d/*.conf; do
[ -f "$f" ] && grep -qw dummy "$f" || continue
sed -i '/\bdummy\b/d' "$f"
grep -q '[^[:space:]]' "$f" || rm -f "$f"
done
[ -f /etc/modules ] && sed -i '/^[[:space:]]*dummy[[:space:]]*$/d' /etc/modules
modprobe -r dummy 2>/dev/null
true
@@ -0,0 +1,4 @@
if grep -rqsw dummy /etc/modules-load.d /etc/modprobe.d /etc/modules; then
refuse "/etc/modules-load.d or /etc/modprobe.d already mentions dummy; remove that first"
fi
modprobe -r dummy 2>/dev/null || true
@@ -0,0 +1,29 @@
# Linux+ 1.2.2: Finding out about hardware
An inventory of the VM's hardware, the kernel's messages as a device
appears, and PCI devices that no driver has claimed.
## Before you start
The lab sets nothing up. Tasks 1 and 3 ask for findings: write them to the
files in `/root/lab-answers/` that the tasks name.
## Tasks
1. Build a quick inventory of this VM with `lscpu`, `lsmem`, `lspci`,
`lsusb`, `lshw` and `dmidecode`. The "hardware" is the hypervisor's: write
the system manufacturer that `dmidecode` reports (such as `QEMU`) to
`/root/lab-answers/1`.
2. Run `dmesg -w` in one terminal, then attach a USB device or a new
virtual disk to the VM, and watch the kernel's messages. Not graded.
3. Find every PCI device without a driver, and write their slot addresses
(such as `00:00.0`), one per line, to `/root/lab-answers/3`. If there are
none, write `none`.
## Hints
- Minimal RHEL-family installs need `pciutils` and `usbutils` for `lspci`
and `lsusb`.
- `dmidecode -t system`, or `dmidecode -s system-manufacturer`.
- A device that a driver has claimed has a `driver` link in sysfs:
`for d in /sys/bus/pci/devices/*; do [ -e "$d/driver" ] || lspci -s "${d##*/}"; done`
@@ -0,0 +1,19 @@
maker=$(dmidecode -s system-manufacturer 2>/dev/null | head -n1 | tr 'A-Z' 'a-z')
# PCI slots without a driver, without the leading domain (0000:).
nodriver=$(for d in /sys/bus/pci/devices/*; do [ -e "$d/driver" ] || echo "${d##*/}"; done | sed 's/^0000://')
# The answer's first words, without a leading domain.
slots() { awk 'NF {print tolower($1)}' /root/lab-answers/3 2>/dev/null | sed 's/^0000://'; }
task 1 "The system manufacturer"
need "/root/lab-answers/1 names the system manufacturer, $maker" '
a=$(answer 1); [ -n "$a" ] && { [ "$a" = "$maker" ] || [[ $maker == *"$a"* ]] || [[ $a == *"$maker"* ]]; }'
task 3 "PCI devices without a driver"
if [ -z "$nodriver" ]; then
need "/root/lab-answers/3 says none" '[ "$(answer 3)" = none ]'
else
need "/root/lab-answers/3 lists every PCI device without a driver" '
[ -n "$(slots)" ] && while read -r s; do slots | grep -qxF "$s" || exit 1; done <<<"$nodriver"'
need "/root/lab-answers/3 lists only devices without a driver" '
slots | while read -r s; do grep -qxF "$s" <<<"$nodriver" || exit 1; done'
fi
@@ -0,0 +1,3 @@
TITLE="1.2.2 Finding out about hardware"
CHAPTER=/linux-plus/1-system-management/1-2-2-hardware-inventory/
SYSTEMS="rhel debian"
@@ -0,0 +1 @@
true
@@ -0,0 +1,2 @@
# Nothing to set up: the tasks look at the system as it is.
true
@@ -0,0 +1,30 @@
# Linux+ 1.3.1: Disks and partitions
GPT partition tables with fdisk and with parted in script mode, and growing
a partition into free space.
## Before you start
The lab adds three empty 2 GiB spare disks: `/dev/loop40`, `/dev/loop41`
and `/dev/loop42`. Their partitions show up as `/dev/loop40p1` and so on.
They come back at every boot. Each task has a disk of its own, so that the
check can see all three results.
## Tasks
1. On `/dev/loop40`, make a GPT partition table with two partitions using
fdisk: the first 500 MiB, the second filling the rest of the disk.
2. On `/dev/loop41`, do the same with parted in script mode (`parted -s`),
naming the partitions `one` and `two`.
3. On `/dev/loop42`, make the same two partitions, then delete partition 2
and grow partition 1 into the free space with `growpart`.
## Hints
- `lsblk` first, to be sure you're on the right disk.
- In fdisk: `g` (new GPT table), `n` (new partition, size `+500M`), `n`
again (accept the defaults for the rest), `p`, `w`.
- `parted -s DISK mklabel gpt mkpart one 1MiB 500MiB mkpart two 500MiB 100%`;
`parted DISK print`.
- `parted -s DISK rm 2`, then `growpart DISK 1` (in `cloud-utils-growpart`
on the RHEL family, `cloud-guest-utils` on Ubuntu).
+27
View File
@@ -0,0 +1,27 @@
# parted's machine-readable listing in MiB: NUMBER:START:END:SIZE:FS:NAME:FLAGS
parts() { parted -m -s "$1" unit MiB print 2>/dev/null | grep -E '^[0-9]+:' | tr -d 'MiB;'; }
label() { parted -m -s "$1" print 2>/dev/null | awk -F: 'NR == 2 {print $6}'; }
# field DISK PART N: field N of partition PART, whole MiB.
field() { parts "$1" | awk -F: -v p="$2" -v n="$3" '$1 == p {printf "%d", $n}'; }
count() { parts "$1" | wc -l; }
between() { [ -n "$1" ] && [ "$1" -ge "$2" ] && [ "$1" -le "$3" ]; }
task 1 "loop40: GPT, 500 MiB and the rest, with fdisk"
need "/dev/loop40 has a GPT partition table" '[ "$(label /dev/loop40)" = gpt ]'
need "/dev/loop40 has two partitions" '[ "$(count /dev/loop40)" = 2 ]'
need "partition 1 is 500 MiB" 'between "$(field /dev/loop40 1 4)" 495 505'
need "partition 2 fills the rest of the disk" 'between "$(field /dev/loop40 2 3)" 2040 2048 && between "$(field /dev/loop40 2 4)" 1500 1550'
task 2 "loop41: GPT with partitions one and two, with parted"
need "/dev/loop41 has a GPT partition table" '[ "$(label /dev/loop41)" = gpt ]'
need "/dev/loop41 has two partitions" '[ "$(count /dev/loop41)" = 2 ]'
need "they're named one and two" '[ "$(parts /dev/loop41 | cut -d: -f6 | paste -sd,)" = one,two ]'
need "partition 1 is about 500 MiB" 'between "$(field /dev/loop41 1 4)" 490 505'
need "partition 2 fills the rest of the disk" 'between "$(field /dev/loop41 2 3)" 2040 2048 && between "$(field /dev/loop41 2 4)" 1500 1560'
task 3 "loop42: partition 2 deleted, partition 1 grown into the space"
need "/dev/loop42 has a GPT partition table" '[ "$(label /dev/loop42)" = gpt ]'
need "/dev/loop42 has only partition 1" '[ "$(parts /dev/loop42 | cut -d: -f1 | paste -sd,)" = 1 ]'
need "partition 1 starts where it did (near the start of the disk)" 'between "$(field /dev/loop42 1 2)" 0 2'
need "partition 1 fills the disk" 'between "$(field /dev/loop42 1 3)" 2040 2048'
need "the kernel sees the grown partition" 'between "$(( $(blockdev --getsize64 /dev/loop42p1 2>/dev/null || echo 0) / 1048576 ))" 2030 2048'
@@ -0,0 +1,3 @@
TITLE="1.3.1 Disks and partitions"
CHAPTER=/linux-plus/1-system-management/1-3-1-partitions/
SYSTEMS="rhel debian"
@@ -0,0 +1,2 @@
# The spare disks, and their partitions, are removed after this.
true
@@ -0,0 +1,3 @@
disk_add 0 2G >/dev/null
disk_add 1 2G >/dev/null
disk_add 2 2G >/dev/null
+30
View File
@@ -0,0 +1,30 @@
# Linux+ 1.3.2: Logical volume management
A volume group with two logical volumes, growing one while it's mounted, and
emptying a disk to take it out of the group.
## Before you start
The lab adds two empty 1 GiB spare disks, `/dev/loop40` and `/dev/loop41`
(the chapter's `vdb` and `vdc`). They come back at every boot. Nothing here
is mounted at boot, so do the tasks in one go, without a reboot, and run
`le-lab check` before you clean up.
## Tasks
1. Make both disks physical volumes in a volume group `vglab`. Create a
600 MiB logical volume `one` and a logical volume `two` from all the rest
of the space. Put ext4 on `one` and mount it at `/mnt/one`.
2. Remove `two`, and grow `one` and its filesystem by 300 MiB while it's
mounted.
3. Move everything off `/dev/loop40` and take it out of `vglab`.
When `le-lab check` passes, clean up as the chapter shows (unmount, remove
the volume group and the physical volumes), or leave it to `le-lab reset`.
## Hints
- `pvcreate`, `vgcreate`, `lvcreate -n -L`, `lvcreate -l 100%FREE`,
`mkfs.ext4`; `pvs`, `vgs`, `lvs`.
- `lvremove`, `lvextend -r -L +300M`; `df -h /mnt/one`.
- `pvs -o+pv_used`, `pvmove`, `vgreduce`.
+18
View File
@@ -0,0 +1,18 @@
lv_mib() { lvs --noheadings --units m --nosuffix -o lv_size "vglab/$1" 2>/dev/null | awk '{printf "%d", $1}'; }
pv_vg() { pvs --noheadings -o vg_name "$1" 2>/dev/null | tr -d ' '; }
task 1 "vglab with an ext4 volume one mounted at /mnt/one"
need "volume group vglab exists" vgs vglab
need "logical volume vglab/one exists" lvs vglab/one
need "one holds an ext4 filesystem" '[ "$(blkid -s TYPE -o value /dev/vglab/one)" = ext4 ]'
need "/mnt/one is mounted from one" '[ "$(findmnt -n -o SOURCE /mnt/one)" = /dev/mapper/vglab-one ]'
task 2 "two removed, one and its filesystem grown by 300 MiB"
need "logical volume two is gone" '! lvs vglab/two'
need "one is 900 MiB (600 + 300)" 'v=$(lv_mib one); [ -n "$v" ] && [ "$v" -ge 896 ] && [ "$v" -le 904 ]'
need "the filesystem on /mnt/one has grown too" '[ "$(df -BM --output=size /mnt/one | tail -n1 | tr -dc 0-9)" -ge 840 ]'
task 3 "/dev/loop40 emptied and taken out of vglab"
need "/dev/loop40 is no longer in vglab" '[ "$(pv_vg /dev/loop40)" != vglab ]'
need "/dev/loop41 is still in vglab" '[ "$(pv_vg /dev/loop41)" = vglab ]'
need "/mnt/one is still mounted from one" '[ "$(findmnt -n -o SOURCE /mnt/one)" = /dev/mapper/vglab-one ]'
+3
View File
@@ -0,0 +1,3 @@
TITLE="1.3.2 Logical volume management"
CHAPTER=/linux-plus/1-system-management/1-3-2-lvm/
SYSTEMS="rhel debian"
+5
View File
@@ -0,0 +1,5 @@
findmnt -rn -o SOURCE /mnt/one 2>/dev/null | grep -q vglab && umount -l /mnt/one
vgchange -an vglab >/dev/null 2>&1
vgremove -ff vglab >/dev/null 2>&1
rmdir /mnt/one 2>/dev/null
true
+4
View File
@@ -0,0 +1,4 @@
vgs vglab >/dev/null 2>&1 && refuse "a volume group vglab already exists; remove it first"
findmnt /mnt/one >/dev/null && refuse "something is mounted at /mnt/one; unmount it first"
disk_add 0 1G >/dev/null
disk_add 1 1G >/dev/null
@@ -0,0 +1,29 @@
# Linux+ 1.3.3: Filesystems
An ext4 and an XFS filesystem, running out of inodes with space to spare, a
filesystem check, and a quick benchmark.
## Before you start
The lab adds two empty 1 GiB spare disks, `/dev/loop40` and `/dev/loop41`
(the chapter's `vdb` and `vdc`). They come back at every boot.
## Tasks
1. Make an ext4 filesystem with only about 2000 inodes on `/dev/loop40`,
and an XFS filesystem on `/dev/loop41`. Mount them at `/mnt/e` and
`/mnt/x`, and compare their space and inodes.
2. Fill `/mnt/e` with empty files until it runs out of inodes while it
still has space. Write the number of files you managed to create to
`/root/lab-answers/2`, then delete them.
3. Unmount `/mnt/e` and check its filesystem with a forced fsck. Install
fio and benchmark `/mnt/x` (random reads and writes of 4 KiB blocks on a
200 MiB file, for 15 seconds), then remove the test file and unmount
`/mnt/x`.
## Hints
- `mkfs.ext4 -N`, `mkfs.xfs`; `df -hT` and `df -i`.
- `for i in $(seq 1 3000); do touch /mnt/e/f$i || break; done`; `ls /mnt/e | wc -l`.
- `fsck -f`; `dumpe2fs -h` shows when a filesystem was last checked.
- `fio --name=t --filename=/mnt/x/t --size=200M --rw=randrw --bs=4k --direct=1 --runtime=15 --time_based`
@@ -0,0 +1,23 @@
sb() { dumpe2fs -h /dev/loop40 2>/dev/null | awk -F: -v k="$1" '$1 == k {sub(/^[ \t]+/, "", $2); s = $2; for (i = 3; i <= NF; i++) s = s ":" $i; print s}'; }
fstype() { blkid -s TYPE -o value "$1" 2>/dev/null; }
inodes=$(sb 'Inode count')
first=$(sb 'First inode')
task 1 "ext4 with about 2000 inodes on loop40, XFS on loop41"
need "/dev/loop40 holds ext4" '[ "$(fstype /dev/loop40)" = ext4 ]'
need "the ext4 filesystem has about 2000 inodes (not tens of thousands)" '[ -n "$inodes" ] && [ "$inodes" -ge 1500 ] && [ "$inodes" -le 4096 ]'
need "/dev/loop41 holds XFS" '[ "$(fstype /dev/loop41)" = xfs ]'
task 2 "/mnt/e run out of inodes, the count noted, the files deleted"
need "/root/lab-answers/2 has the number of files that fit ($((inodes - first)))" '
a=$(answer 2 | tr -dc 0-9); [ -n "$a" ] && [ "$a" -ge $((inodes - first - 2)) ] && [ "$a" -le $((inodes - first + 1)) ]'
need "the files are deleted again" '[ "$(sb "Free inodes")" -ge $((inodes - first - 2)) ]'
task 3 "ext4 checked with fsck, XFS benchmarked with fio"
need "/dev/loop40 isn't mounted" '! findmnt -S /dev/loop40'
need "fsck checked /dev/loop40 after it was made" '[ "$(date -d "$(sb "Last checked")" +%s)" -gt "$(date -d "$(sb "Filesystem created")" +%s)" ]'
need "fio is installed" 'command -v fio'
need "/dev/loop41 isn't mounted" '! findmnt -S /dev/loop41'
need "the test file is removed" '
m=$(mktemp -d); mount -o ro /dev/loop41 "$m" || exit 1
n=$(find "$m" -mindepth 1 | wc -l); umount "$m"; rmdir "$m"; [ "$n" = 0 ]'
@@ -0,0 +1,3 @@
TITLE="1.3.3 Filesystems"
CHAPTER=/linux-plus/1-system-management/1-3-3-filesystems/
SYSTEMS="rhel debian"
@@ -0,0 +1,5 @@
for m in /mnt/e /mnt/x; do
findmnt -rn -o SOURCE "$m" 2>/dev/null | grep -q loop4 && umount -l "$m"
rmdir "$m" 2>/dev/null
done
true
@@ -0,0 +1,5 @@
for m in /mnt/e /mnt/x; do
findmnt "$m" >/dev/null && refuse "something is mounted at $m; unmount it first"
done
disk_add 0 1G >/dev/null
disk_add 1 1G >/dev/null
+32
View File
@@ -0,0 +1,32 @@
# Linux+ 1.3.4: Mounting filesystems
An `/etc/fstab` line by UUID with mount options, `noexec` in action, a
remount, and finding what keeps a mount busy.
## Before you start
The lab adds a 512 MiB spare disk, `/dev/loop40`, with an ext4 filesystem on
it. It comes back at every boot. Task 2 changes the mount only until it's
mounted again, so do the tasks in one go and run `le-lab check` before you
clean up.
## Tasks
1. Add `/dev/loop40` to `/etc/fstab` by its UUID, mounted at `/mnt/lab` with
the options `defaults,noatime,noexec,nofail`, and mount it from fstab.
2. Copy `/usr/bin/true` into `/mnt/lab` and show that it won't run from
there. Then remount `/mnt/lab` so that programs can run from it, without
changing `/etc/fstab`.
3. Make the mount busy (`cd /mnt/lab`), try to unmount it, and find the
culprit with `lsof`. Not graded.
When `le-lab check` passes, remove your line from `/etc/fstab`, run
`systemctl daemon-reload` and unmount, or leave it to `le-lab reset`.
## Hints
- `blkid /dev/loop40`, `blkid -s UUID -o value`.
- After editing fstab: `systemctl daemon-reload`, `mount -a`, `findmnt /mnt/lab`.
- `mount -o remount,exec`.
- `lsof +D /mnt/lab`, `fuser -vm /mnt/lab`. Minimal RHEL-family installs need
the `lsof` package.
+20
View File
@@ -0,0 +1,20 @@
UUID=$(state_get uuid)
# The fstab line for /mnt/lab, comments left out.
line() { awk '$1 !~ /^#/ && $2 == "/mnt/lab"' /etc/fstab; }
has_opt() { line | awk '{print $4}' | tr , '\n' | grep -qx "$1"; }
live_opt() { findmnt -n -o OPTIONS /mnt/lab | tr , '\n' | grep -qx "$1"; }
task 1 "/dev/loop40 in fstab by UUID at /mnt/lab with noatime,noexec,nofail"
need "/etc/fstab has one line for /mnt/lab" '[ "$(line | wc -l)" = 1 ]'
need "the line names the disk by UUID=$UUID" '[ "$(line | awk "{print \$1}" | tr -d "\"")" = "UUID=$UUID" ]'
need "the line has ext4 and the options noatime, noexec and nofail" '[ "$(line | awk "{print \$3}")" = ext4 ] && has_opt noatime && has_opt noexec && has_opt nofail'
need "/mnt/lab is mounted from /dev/loop40" '[ "$(findmnt -n -o SOURCE /mnt/lab)" = /dev/loop40 ]'
need "the mount uses noatime" 'live_opt noatime'
task 2 "true copied in; /mnt/lab remounted to allow programs"
need "/mnt/lab/true is there" '[ -f /mnt/lab/true ] && [ -x /mnt/lab/true ]'
need "/mnt/lab is mounted without noexec now" 'findmnt /mnt/lab && ! live_opt noexec'
need "/mnt/lab/true runs" /mnt/lab/true
need "/etc/fstab still says noexec" 'has_opt noexec'
task 3 "The busy mount's culprit found with lsof" self
+3
View File
@@ -0,0 +1,3 @@
TITLE="1.3.4 Mounting filesystems"
CHAPTER=/linux-plus/1-system-management/1-3-4-mounting/
SYSTEMS="rhel debian"
+7
View File
@@ -0,0 +1,7 @@
UUID=$(state_get uuid)
[ -n "$UUID" ] && sed -i "/$UUID/d" /etc/fstab
sed -i -E '/^[^#]*[[:space:]]\/mnt\/lab[[:space:]]/d' /etc/fstab
systemctl daemon-reload
findmnt /mnt/lab >/dev/null && umount -l /mnt/lab
rmdir /mnt/lab 2>/dev/null
true
+5
View File
@@ -0,0 +1,5 @@
findmnt /mnt/lab >/dev/null && refuse "something is mounted at /mnt/lab; unmount it first"
grep -Eq '^[^#]*[[:space:]]/mnt/lab[[:space:]]' /etc/fstab && refuse "/etc/fstab already has a line for /mnt/lab; remove it first"
disk_add 0 512M >/dev/null
mkfs.ext4 -q /dev/loop40
state_set uuid "$(blkid -s UUID -o value /dev/loop40)"
+33
View File
@@ -0,0 +1,33 @@
# Linux+ 1.3.5: Software RAID
A RAID 1 mirror with a hot spare, a failed disk, the spare taking over, and
the old disk coming back as the new spare.
## Before you start
The lab installs `mdadm` and adds three empty 1 GiB spare disks,
`/dev/loop40`, `/dev/loop41` and `/dev/loop42` (the chapter's `vdb`, `vdc`
and `vdd`). They come back at every boot, but the array isn't set up to
start and mount at boot, so do the tasks in one go and run `le-lab check`
before you clean up.
## Tasks
1. Build `/dev/md0`, a RAID 1 of `/dev/loop40` and `/dev/loop41` with
`/dev/loop42` as a spare. Put ext4 on it, mount it at `/mnt/raid`, and
write `hello` to `/mnt/raid/file`.
2. Mark `/dev/loop40` as failed and watch the spare take over. The file is
still readable.
3. Remove the failed disk from the array, wipe its RAID superblock, and add
it back as the new spare.
When `le-lab check` passes, clean up as the chapter shows (unmount, stop the
array, wipe the superblocks), or leave it to `le-lab reset`.
## Hints
- `mdadm --create /dev/md0 --level=1 --raid-devices=2 --spare-devices=1 --metadata=1.2 ...`;
`cat /proc/mdstat`; `mdadm --wait /dev/md0` waits for a sync to finish.
- `mdadm /dev/md0 --fail DISK`; `mdadm --detail /dev/md0`.
- `mdadm /dev/md0 --remove DISK`, `mdadm --zero-superblock DISK`,
`mdadm /dev/md0 --add DISK`.
+22
View File
@@ -0,0 +1,22 @@
# A disk's role in md0 as mdadm --detail shows it: "active sync", "spare",
# "faulty", or nothing.
role() { mdadm --detail /dev/md0 2>/dev/null | awk -v d="$1" '$NF == d {$1 = $2 = $3 = $4 = $NF = ""; gsub(/^ +| +$/, ""); print}'; }
detail() { mdadm --detail /dev/md0 2>/dev/null | awk -F' : ' -v k="$1" '{gsub(/^ +/, "", $1)} $1 == k {print $2}'; }
task 1 "md0: RAID 1 with a spare, ext4, mounted at /mnt/raid, file says hello"
need "/dev/md0 is a RAID 1 array" '[ "$(detail "Raid Level")" = raid1 ]'
need "md0 mirrors two disks" '[ "$(detail "Raid Devices")" = 2 ]'
need "md0 holds ext4" '[ "$(blkid -s TYPE -o value /dev/md0)" = ext4 ]'
need "/mnt/raid is mounted from md0" '[ "$(findmnt -n -o SOURCE /mnt/raid)" = /dev/md0 ]'
need "/mnt/raid/file says hello" 'grep -qx hello /mnt/raid/file'
task 2 "loop40 failed, the spare loop42 took over"
need "/dev/loop42 is an active member of md0" '[[ $(role /dev/loop42) == "active sync"* ]]'
need "/dev/loop41 is still an active member" '[[ $(role /dev/loop41) == "active sync"* ]]'
need "md0 has finished rebuilding (clean, not degraded)" '
s=$(detail State); [[ $s == clean* || $s == active* ]] && [[ $s != *degraded* && $s != *recovering* && $s != *resyncing* ]]'
need "/mnt/raid/file still says hello" 'grep -qx hello /mnt/raid/file'
task 3 "loop40 removed, wiped and added back as the spare"
need "/dev/loop40 is md0's spare" '[ "$(role /dev/loop40)" = spare ]'
need "md0 has no failed disks" '[ "$(detail "Failed Devices")" = 0 ]'
+3
View File
@@ -0,0 +1,3 @@
TITLE="1.3.5 Software RAID"
CHAPTER=/linux-plus/1-system-management/1-3-5-raid/
SYSTEMS="rhel debian"
+11
View File
@@ -0,0 +1,11 @@
findmnt /mnt/raid >/dev/null && umount -l /mnt/raid
# Stop every array built from the spare disks, then wipe their superblocks.
for md in /sys/block/md*; do
[ -e "$md" ] || continue
ls "$md/slaves" 2>/dev/null | grep -q '^loop4[0-9]' && mdadm --stop "/dev/${md##*/}" >/dev/null 2>&1
done
for dev in /dev/loop40 /dev/loop41 /dev/loop42; do
[ -e "$dev" ] && mdadm --zero-superblock "$dev" >/dev/null 2>&1
done
rmdir /mnt/raid 2>/dev/null
true
+7
View File
@@ -0,0 +1,7 @@
[ -e /dev/md0 ] && [ -n "$(cat /sys/block/md0/md/array_state 2>/dev/null | grep -v clear)" ] &&
refuse "/dev/md0 is already in use; stop it first"
findmnt /mnt/raid >/dev/null && refuse "something is mounted at /mnt/raid; unmount it first"
command -v mdadm >/dev/null || pkg_install mdadm
disk_add 0 1G >/dev/null
disk_add 1 1G >/dev/null
disk_add 2 1G >/dev/null
@@ -0,0 +1,38 @@
# Linux+ 1.4.1: Network configuration
How names are resolved, and a second address on an interface: with
NetworkManager on the RHEL family, with a Netplan file on Ubuntu.
## Before you start
So that you never touch the interface you're connected over, the lab adds a
practice interface, `lab0` (a dummy device, with nothing on the other end).
On the RHEL family it's NetworkManager connection `lab0`, with the address
`192.168.76.1/24`. On Ubuntu, systemd-networkd creates it with no address.
It comes back at every boot. Use `lab0` wherever the chapter uses your main
connection or interface.
## Tasks
1. Make the name `labtest` resolve to `10.99.99.99` without DNS. Check it
with `getent hosts labtest`, and see that `dig +short labtest` (which
only asks DNS) doesn't find it. Leave the line in place until you've run
`le-lab check`.
2. RHEL family: add a second address, `192.168.77.10/24`, to connection
`lab0`, keeping the first, and bring the connection up again.
3. Ubuntu: give `lab0` the address `192.168.77.10/24` with a new Netplan
file, `/etc/netplan/90-lab.yaml`, readable only by root, and apply it
with `netplan try`.
When `le-lab check` passes, take the line and the address out again as the
chapter shows, or leave it to `le-lab reset`.
## Hints
- `/etc/resolv.conf`, the `hosts:` line in `/etc/nsswitch.conf`,
`/etc/hosts`. On the RHEL family, `dig` is in `bind-utils`.
- `nmcli -t -f NAME,DEVICE con show --active`,
`nmcli con mod NAME +ipv4.addresses ...`, `nmcli con up NAME`,
`ip -br addr`.
- An `ethernets:` entry for `lab0` with `addresses: [...]`; `chmod 600`;
`netplan try` (press Enter to keep it), `netplan status`.
@@ -0,0 +1,20 @@
has_live() { ip -o -4 addr show dev lab0 2>/dev/null | grep -qF " $1 "; }
task 1 "labtest resolves to 10.99.99.99 without DNS"
need "getent hosts labtest prints 10.99.99.99" 'getent hosts labtest | grep -q "^10\.99\.99\.99[[:space:]]"'
need "/etc/hosts has the line" 'grep -Eq "^10\.99\.99\.99[[:space:]].*\blabtest\b" /etc/hosts'
task 2 "Connection lab0 has 192.168.77.10/24 as well" rhel
if is_rhel; then
need "connection lab0 has 192.168.77.10/24" 'nmcli -g ipv4.addresses connection show lab0 | grep -qF 192.168.77.10/24'
need "connection lab0 keeps 192.168.76.1/24" 'nmcli -g ipv4.addresses connection show lab0 | grep -qF 192.168.76.1/24'
need "lab0 has both addresses now (connection up)" 'has_live 192.168.77.10/24 && has_live 192.168.76.1/24'
fi
task 3 "lab0 has 192.168.77.10/24 from /etc/netplan/90-lab.yaml" debian
if is_debian; then
need "/etc/netplan/90-lab.yaml exists" '[ -f /etc/netplan/90-lab.yaml ]'
need "only root can read it (mode 600)" '[ "$(stat -c %a /etc/netplan/90-lab.yaml)" = 600 ]'
need "it gives lab0 192.168.77.10/24" 'netplan get ethernets.lab0.addresses 2>/dev/null | grep -qF 192.168.77.10/24'
need "lab0 has 192.168.77.10/24 now (applied)" 'has_live 192.168.77.10/24'
fi
@@ -0,0 +1,3 @@
TITLE="1.4.1 Network configuration"
CHAPTER=/linux-plus/1-system-management/1-4-1-network-configuration/
SYSTEMS="rhel debian"
@@ -0,0 +1,10 @@
sed -i -E '/^[^#]*[[:space:]]labtest([[:space:]]|$)/d' /etc/hosts
if is_rhel; then
nmcli connection delete lab0 >/dev/null 2>&1
else
rm -f /etc/netplan/90-lab.yaml /etc/systemd/network/10-le-lab0.netdev
netplan generate 2>/dev/null
networkctl reload 2>/dev/null
fi
ip link del lab0 2>/dev/null
true
@@ -0,0 +1,23 @@
[ -e /sys/class/net/lab0 ] && refuse "an interface lab0 already exists; remove it first"
if is_rhel; then
command -v nmcli >/dev/null && systemctl is-active --quiet NetworkManager ||
refuse "this lab needs NetworkManager running"
nmcli -g NAME connection show | grep -qx lab0 && refuse "a NetworkManager connection lab0 already exists; delete it first"
else
command -v netplan >/dev/null && systemctl is-active --quiet systemd-networkd ||
refuse "this lab needs Netplan with systemd-networkd"
[ -e /etc/netplan/90-lab.yaml ] && refuse "/etc/netplan/90-lab.yaml already exists; remove it first"
fi
grep -Eq '^[^#]*[[:space:]]labtest([[:space:]]|$)' /etc/hosts && refuse "/etc/hosts already has labtest; remove that line first"
if is_rhel; then
nmcli -w 20 connection add type dummy ifname lab0 con-name lab0 \
ipv4.method manual ipv4.addresses 192.168.76.1/24 ipv6.method disabled >/dev/null
nmcli -w 20 connection up lab0 >/dev/null
else
mkdir -p /etc/systemd/network
printf '[NetDev]\nName=lab0\nKind=dummy\n' >/etc/systemd/network/10-le-lab0.netdev
networkctl reload
for i in $(seq 20); do [ -e /sys/class/net/lab0 ] && break; sleep 0.5; done
[ -e /sys/class/net/lab0 ]
fi
@@ -0,0 +1,40 @@
# Linux+ 1.4.2: Network tools
Mapping the VM's network, following a lookup and a connection, capturing
DNS traffic, and testing a port and throughput between two machines.
## Before you start
The peer, at `10.99.0.2` (name `peer`), stands in for the other VM. It sits
on this VM's second network, `10.99.0.0/24`, where this VM is `10.99.0.1`.
It shares this VM's files and installed programs, but has its own network
stack: get a shell there with `sudo ip netns exec lepeer bash`. Tasks 1 and
2 ask for findings: write them to the files in `/root/lab-answers/` that
they name. Task 2 needs internet access. Reset closes the ports again and
removes `iperf3` and `nc` if they weren't installed before the lab.
## Tasks
1. Map this VM's network: addresses, routes, neighbors, listening ports.
Write the address of the default gateway to `/root/lab-answers/1`.
2. Look up `www.kernel.org`, trace the path to it, and fetch the headers of
`https://www.kernel.org`. Write the first line of the headers (the
status line, such as `HTTP/1.1 301 Moved Permanently`) to
`/root/lab-answers/2`.
3. Capture your own DNS traffic with `tcpdump` while you look up a name.
Not graded.
4. Test a port and the throughput between this VM (VM A) and the peer
(VM B). Install `iperf3` and `nc`, and if a firewall is running here,
open ports 9000/tcp and 5201/tcp until the next reboot. Run `nc -l 9000`
here and `nc -zv 10.99.0.1 9000` in the peer, then `iperf3 -s` here and
`iperf3 -c 10.99.0.1` in the peer.
## Hints
- `ip -br a`, `ip r`, `ip neigh`, `ss -tulnp`, `hostname -I`.
- On the RHEL family, `dig` is in `bind-utils`, and `nc` in `nmap-ncat`.
`dig +short`, `tracepath -n`, `curl -sI URL | head -1`.
- `tcpdump -i any -nn port 53` in one terminal, `dig example.org` in
another; Ctrl+c stops tcpdump.
- `firewall-cmd --add-port=9000/tcp --add-port=5201/tcp` (without
`--permanent`); with ufw, `ufw allow 9000/tcp`.
@@ -0,0 +1,30 @@
gw=$(ip -4 route show default | awk '{print $3; exit}')
# reach PORT: the peer can open a TCP connection to PORT here. If nothing
# listens there, a listener runs for a moment so that the firewall is tested.
reach() {
local pid='' ok=1
if ! listening "$1" tcp; then
python3 -c 'import socket,sys,time
s=socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", int(sys.argv[1]))); s.listen(5); time.sleep(12)' "$1" &
pid=$!
for i in 1 2 3 4 5 6 7 8 9 10; do listening "$1" tcp && break; sleep 0.3; done
fi
in_peer timeout 6 bash -c "</dev/tcp/10.99.0.1/$1" && ok=0
[ -n "$pid" ] && kill "$pid" 2>/dev/null
return $ok
}
task 1 "The default gateway's address"
need "/root/lab-answers/1 has the default gateway, $gw" '[ "$(answer 1)" = "$gw" ]'
task 2 "The status line of https://www.kernel.org"
need "/root/lab-answers/2 has an HTTP status line" 'answer 2 | grep -Eq "^http/[0-9.]+ [1-5][0-9][0-9]"'
task 3 "DNS traffic captured with tcpdump" self
task 4 "iperf3 and nc installed; the peer reaches ports 9000 and 5201 here"
need "iperf3 is installed" 'command -v iperf3'
need "nc is installed" 'command -v nc'
need "the peer can connect to port 9000/tcp here" 'reach 9000'
need "the peer can connect to port 5201/tcp here" 'reach 5201'
@@ -0,0 +1,3 @@
TITLE="1.4.2 Network tools"
CHAPTER=/linux-plus/1-system-management/1-4-2-network-tools/
SYSTEMS="rhel debian"
@@ -0,0 +1,19 @@
if systemctl is-active --quiet firewalld; then
for p in 9000 5201; do
[ -n "$(state_get fw-had-$p)" ] || firewall-cmd -q --remove-port=$p/tcp 2>/dev/null
done
elif command -v ufw >/dev/null && ufw status | grep -q 'Status: active'; then
for p in 9000 5201; do
[ -n "$(state_get ufw-had-$p)" ] || { ufw delete allow $p/tcp; ufw delete allow $p; } >/dev/null 2>&1
done
fi
true
# The packages task 4 installed, if they weren't there before the lab.
if [ -s "$LAB_STATE/not-installed" ]; then
if is_rhel; then
dnf -y -q remove $(cat "$LAB_STATE/not-installed") >/dev/null 2>&1
else
DEBIAN_FRONTEND=noninteractive apt-get -y -qq purge $(cat "$LAB_STATE/not-installed") >/dev/null 2>&1
fi
fi
true
@@ -0,0 +1,14 @@
IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}')
[ -n "$IF" ] || refuse "no default route; this lab needs a working network"
peer_up
# What the firewall allowed before, so that reset takes out only the lab's ports.
if systemctl is-active --quiet firewalld; then
for p in 9000 5201; do firewall-cmd -q --query-port=$p/tcp && state_set fw-had-$p 1; done
elif command -v ufw >/dev/null && ufw status | grep -q 'Status: active'; then
for p in 9000 5201; do ufw status | grep -Eq "^$p(/tcp)?[[:space:]].*ALLOW" && state_set ufw-had-$p 1; done
fi
true
# Reset takes out again what task 4 installs, so that the lab starts over.
for p in iperf3 $(pick nmap-ncat netcat-openbsd); do
pkg_installed "$p" || printf '%s\n' "$p" >>"$LAB_STATE/not-installed"
done
@@ -0,0 +1,32 @@
# Linux+ 1.5.1: The shell environment
A directory of your own on the PATH for good, which startup files bash runs
for login and non-login shells, and the history shortcuts.
## Before you start
The lab creates user `alex` (password `Lab-Pass-1`, shell bash). Do the
tasks as `alex`: `sudo -iu alex` or `su - alex`. On the RHEL family alex's
login startup file is `~/.bash_profile`; on Ubuntu it's `~/.profile`.
## Tasks
1. Create `~/bin/hi`, a script that prints `hi from` and its own name
(`echo "hi from $0"`), and make it executable. Add `$HOME/bin` to the
front of `PATH` in alex's login startup file, so that `hi` works in
every new login shell.
2. Add the line `echo bashrc ran` to `~/.bashrc` and `echo profile ran` to
the login startup file. Start `bash` (non-login) and `bash -l` (login)
and see which lines each one prints. Leave the lines in place until
you've run `le-lab check`, then remove them.
3. Practice history: run `ls /etc/ssh`, then `cd !$`, then `!!`, then
press Ctrl+r and type `ls` to find the first command. Not graded.
## Hints
- `mkdir -p ~/bin`, `printf '#!/bin/bash\necho "hi from $0"\n' > ~/bin/hi`,
`chmod +x`.
- `echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bash_profile` (or
`~/.profile`), then `source` it; `which hi`.
- bash runs only the first of `~/.bash_profile`, `~/.bash_login` and
`~/.profile` that it finds for a login shell.
@@ -0,0 +1,21 @@
H=$(getent passwd alex | cut -d: -f6)
# The login startup file bash reads for alex: the first that exists.
P=''
for f in .bash_profile .bash_login .profile; do [ -f "$H/$f" ] && { P=$H/$f; break; }; done
# Output of a shell as alex, with no terminal.
as_alex() { runuser -u alex -- env -i HOME="$H" USER=alex LOGNAME=alex SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin TERM=dumb "$@" </dev/null 2>/dev/null; }
task 1 "~/bin/hi on alex's PATH in every login shell"
need "$H/bin/hi exists and is executable" '[ -f "$H/bin/hi" ] && [ -x "$H/bin/hi" ]'
need "$H/bin/hi belongs to alex" '[ "$(stat -c %U "$H/bin/hi")" = alex ]'
need "alex's login startup file adds \$HOME/bin to PATH" '[ -n "$P" ] && grep -Eq "^[^#]*PATH=[\"]?(\\\$HOME|~|\\\$\{HOME\}|$H)/bin:" "$P"'
need "hi works in a new login shell and prints \"hi from\"" 'as_alex bash -l -c hi | grep -q "hi from .*hi"'
task 2 "echo lines in ~/.bashrc and the login startup file"
need "~/.bashrc has echo bashrc ran" 'grep -Eq "^[[:space:]]*echo [\"'"'"']?bashrc ran" "$H/.bashrc"'
need "the login startup file has echo profile ran" '[ -n "$P" ] && grep -Eq "^[[:space:]]*echo [\"'"'"']?profile ran" "$P"'
need "a login shell prints profile ran" 'as_alex bash -l -c true | grep -qx "profile ran"'
need "a non-login interactive shell prints bashrc ran but not profile ran" '
o=$(as_alex bash -i -c true); grep -qx "bashrc ran" <<<"$o" && ! grep -qx "profile ran" <<<"$o"'
task 3 "History: !\$, !! and Ctrl+r" self
@@ -0,0 +1,3 @@
TITLE="1.5.1 The shell environment"
CHAPTER=/linux-plus/1-system-management/1-5-1-shell-environment/
SYSTEMS="rhel debian"
@@ -0,0 +1,2 @@
# users_remove takes alex and his home directory away after this.
true
@@ -0,0 +1 @@
user_add alex -s /bin/bash
@@ -0,0 +1,34 @@
# Linux+ 1.5.2: Redirection and pipes
Output and errors in separate files, here documents with and without
expansion, `tee`, and file names with spaces through `xargs`.
## Before you start
The lab clears away the files the tasks create, if they're there. Several
are in `/tmp`, which Ubuntu empties at boot, so do the tasks without a
reboot and run `le-lab check` before you clean up.
## Tasks
1. Make a directory `/tmp/redir` and work in it. Run
`ls /etc /nonexistent` four ways: output to `out.txt` and errors to
`err.txt`; both into `all.txt`; and with `2>&1 > only-out.txt`. Watch
where the error goes in the last one.
2. With here documents, write `user=$USER` to `/tmp/a.txt` with the
variable expanded, and to `/tmp/b.txt` exactly as written.
3. Count the login shells in `/etc/passwd`, most common first. Then save
`getent passwd` to `/tmp/users.txt` and count its lines in the same
pipeline.
4. Make a directory `/tmp/x` with two files, `a b.tmp` and `c d.tmp`. See
`find ... | xargs rm` fail on the spaces, then remove them safely with
`find` and `xargs`. Leave `/tmp/x` itself.
## Hints
- `> out.txt 2> err.txt`, `> all.txt 2>&1`; redirections apply left to
right.
- `<< EOF` expands variables; `<< 'EOF'` doesn't.
- `cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn`;
`getent passwd | tee /tmp/users.txt | wc -l`.
- `find /tmp/x -name '*.tmp' -print0 | xargs -0 rm`.
@@ -0,0 +1,18 @@
R=/tmp/redir
task 1 "Output and errors in out.txt, err.txt, all.txt and only-out.txt"
need "$R/err.txt has the error and no listing" 'grep -q nonexistent $R/err.txt && ! grep -qx passwd $R/err.txt'
need "$R/out.txt has the listing of /etc and no error" 'grep -qx passwd $R/out.txt && ! grep -q nonexistent $R/out.txt'
need "$R/all.txt has both" 'grep -qx passwd $R/all.txt && grep -q nonexistent $R/all.txt'
need "$R/only-out.txt has the listing but not the error" 'grep -qx passwd $R/only-out.txt && ! grep -q nonexistent $R/only-out.txt'
task 2 "Here documents: /tmp/a.txt expanded, /tmp/b.txt as written"
need "/tmp/a.txt says user= and a user name" 'grep -Eqx "user=[a-z_][a-z0-9_.-]*" /tmp/a.txt'
need "/tmp/b.txt says user=\$USER" 'grep -qxF "user=\$USER" /tmp/b.txt'
task 3 "getent passwd saved to /tmp/users.txt with tee"
need "/tmp/users.txt is the output of getent passwd" 'getent passwd | cmp -s - /tmp/users.txt'
task 4 "The files with spaces in /tmp/x removed with find and xargs"
need "/tmp/x is there" '[ -d /tmp/x ]'
need "/tmp/x has no .tmp files left" '[ -z "$(find /tmp/x -name "*.tmp")" ]'
@@ -0,0 +1,3 @@
TITLE="1.5.2 Redirection and pipes"
CHAPTER=/linux-plus/1-system-management/1-5-2-redirection-and-pipes/
SYSTEMS="rhel debian"
@@ -0,0 +1,2 @@
rm -rf /tmp/redir /tmp/a.txt /tmp/b.txt /tmp/users.txt /tmp/x
true
@@ -0,0 +1 @@
rm -rf /tmp/redir /tmp/a.txt /tmp/b.txt /tmp/users.txt /tmp/x
@@ -0,0 +1,34 @@
# Linux+ 1.5.3: Working with text
grep, cut, awk, sed, tr, sort and uniq on real files: users with a login
shell, a config file without its comments, an edit with sed, and a word
count.
## Before you start
The lab clears away the files the tasks create in `/tmp`, if they're there.
Ubuntu empties `/tmp` at boot, so do the tasks without a reboot. Tasks 1, 2
and 4 ask for findings: write them to the files in `/root/lab-answers/` that
they name.
## Tasks
1. Find the users with a UID from 1000 to 59999 (the regular users) with
awk, and write their names, one per line, to `/root/lab-answers/1`.
2. Strip the comments and blank lines from `/etc/ssh/sshd_config`, saving
what's left to `/tmp/sshd.active`, and count its lines in the same
pipeline. Write the count to `/root/lab-answers/2`.
3. Copy `/etc/ssh/sshd_config` to `/tmp/s.conf`. With `sed -i.bak`, change
the `PermitRootLogin` line (commented out or not) to
`PermitRootLogin no`, keeping the original as `/tmp/s.conf.bak`, and
compare the two with `diff`.
4. Find the most common word in the bash man page, and write it to
`/root/lab-answers/4`.
## Hints
- `awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd`.
- `grep -v -E '^\s*(#|$)'`, `tee`, `wc -l`. On the RHEL family only root
can read `sshd_config`: `sudo`.
- `sed -i.bak 's/^#\?PermitRootLogin.*/PermitRootLogin no/'`.
- `man bash | tr -cs 'A-Za-z' '\n' | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn | head`.
+22
View File
@@ -0,0 +1,22 @@
regular=$(awk -F: '$3 >= 1000 && $3 < 60000 {print $1}' /etc/passwd | sort)
active() { grep -v -E '^\s*(#|$)' /etc/ssh/sshd_config; }
# The three most common words in the bash man page.
top=$(MANWIDTH=80 man bash 2>/dev/null | tr -cs 'A-Za-z' '\n' | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn | head -n3 | awk '{print $2}')
task 1 "The regular users (UID 1000 to 59999)"
need "/root/lab-answers/1 lists exactly the regular users: $(echo $regular)" '[ "$(awk "NF {print \$1}" /root/lab-answers/1 | sort)" = "$regular" ]'
task 2 "sshd_config without comments in /tmp/sshd.active, and its line count"
need "/tmp/sshd.active is sshd_config without comments and blank lines" 'active | cmp -s - /tmp/sshd.active'
need "/root/lab-answers/2 has the count, $(active | wc -l)" '[ "$(answer 2 | tr -dc 0-9)" = "$(active | wc -l)" ]'
task 3 "PermitRootLogin no in /tmp/s.conf, the original in /tmp/s.conf.bak"
need "/tmp/s.conf.bak is a copy of sshd_config" 'cmp -s /etc/ssh/sshd_config /tmp/s.conf.bak'
need "/tmp/s.conf has the line PermitRootLogin no" 'grep -qx "PermitRootLogin no" /tmp/s.conf'
need "only the PermitRootLogin line changed" '
[ "$(wc -l </tmp/s.conf)" = "$(wc -l </etc/ssh/sshd_config)" ] &&
! diff /tmp/s.conf.bak /tmp/s.conf | grep "^[<>]" | grep -v PermitRootLogin | grep -q .'
task 4 "The most common word in the bash man page"
need "man bash works" '[ -n "$top" ]'
need "/root/lab-answers/4 has the most common word" '[ -n "$(answer 4)" ] && grep -qxF "$(answer 4)" <<<"$top"'
@@ -0,0 +1,3 @@
TITLE="1.5.3 Working with text"
CHAPTER=/linux-plus/1-system-management/1-5-3-text-tools/
SYSTEMS="rhel debian"
@@ -0,0 +1,2 @@
rm -f /tmp/sshd.active /tmp/s.conf /tmp/s.conf.bak
true
@@ -0,0 +1 @@
rm -f /tmp/sshd.active /tmp/s.conf /tmp/s.conf.bak
@@ -0,0 +1,34 @@
# Linux+ 1.5.4: Text editors
Vim's basic moves on a real file, a change in nano, and your preferred
editor for programs such as `crontab -e`.
## Before you start
The lab removes `/tmp/s.conf` if it's there, and notes the startup files
task 4 may change so that reset can put them back. Ubuntu empties `/tmp` at
boot, so do task 2 without a reboot.
## Tasks
1. Run `vimtutor` and work through lessons 1 and 2 (about 15 minutes). Not
graded.
2. Copy `/etc/ssh/sshd_config` to `/tmp/s.conf` and open it in Vim. Turn on
line numbers, jump to the first line with `Port`, copy it, paste it
below, and change the copy to `Port 2222`. Replace every `yes` with
`YES`, undo that, and save and quit.
3. Make the same kind of change to `/tmp/s.conf` in nano, then quit without
saving. Not graded.
4. Set your preferred editor for good (with `EDITOR` or `VISUAL` in a shell
startup file, or with `select-editor` on Ubuntu), and try it with
`crontab -e`; quit without saving.
## Hints
- On the RHEL family `vim` and `vimtutor` are in `vim-enhanced`, and nano
is in `nano`.
- `:set number`, `/Port`, `yy`, `p`, `cw` or `C`, `:%s/yes/YES/g`, `u`,
`:wq`.
- In nano, `Ctrl+x` then `n` quits without saving.
- `export EDITOR=nano` in `~/.bashrc`; `crontab -e` reads `VISUAL`, then
`EDITOR`.
@@ -0,0 +1,16 @@
task 1 "vimtutor lessons 1 and 2" self
task 2 "/tmp/s.conf: the Port line copied and changed to Port 2222, nothing else"
need "/tmp/s.conf exists" '[ -f /tmp/s.conf ]'
need "/tmp/s.conf has the line Port 2222" 'grep -qx "Port 2222" /tmp/s.conf'
need "the only change from sshd_config is the new Port 2222 line" '
[ "$(diff /etc/ssh/sshd_config /tmp/s.conf | grep "^[<>]")" = "> Port 2222" ]'
need "the new line is right below a line with Port" '
grep -B1 -x "Port 2222" /tmp/s.conf | head -n1 | grep -q Port'
task 3 "A change in nano, quit without saving" self
task 4 "A preferred editor set for good"
need "EDITOR or VISUAL is set in a startup file, or select-editor was used" '
grep -Eqs "^[[:space:]]*(export[[:space:]]+)?(EDITOR|VISUAL)=" /etc/environment /etc/profile.d/*.sh /root/.bashrc /root/.bash_profile /root/.profile /home/*/.bashrc /home/*/.bash_profile /home/*/.profile ||
ls /root/.selected_editor /home/*/.selected_editor >/dev/null 2>&1'
@@ -0,0 +1,3 @@
TITLE="1.5.4 Text editors"
CHAPTER=/linux-plus/1-system-management/1-5-4-text-editors/
SYSTEMS="rhel debian"
@@ -0,0 +1,13 @@
rm -f /tmp/s.conf
# Startup files as they were before the lab.
if [ -f "$LAB_STATE/kept" ]; then
while read -r f; do
[ -e "$LAB_STATE/saved$f" ] || continue
rm -rf "$f"; cp -a "$LAB_STATE/saved$f" "$f"
done <"$LAB_STATE/kept"
fi
if [ -f "$LAB_STATE/absent" ]; then
while read -r f; do case $f in *'*'*) ;; *) rm -rf "$f" ;; esac; done <"$LAB_STATE/absent"
fi
command -v restorecon >/dev/null && restorecon -R /etc/environment /etc/profile.d /root /home 2>/dev/null
true
@@ -0,0 +1,13 @@
rm -f /tmp/s.conf
# The startup files task 4 may change, for reset to put back.
saved=$LAB_STATE/saved
for f in /etc/environment /etc/profile.d /root/.bashrc /root/.bash_profile /root/.profile /root/.selected_editor \
/home/*/.bashrc /home/*/.bash_profile /home/*/.profile /home/*/.selected_editor; do
if [ -e "$f" ]; then
mkdir -p "$saved${f%/*}"
cp -a "$f" "$saved$f"
printf '%s\n' "$f" >>"$LAB_STATE/kept"
else
printf '%s\n' "$f" >>"$LAB_STATE/absent"
fi
done
@@ -0,0 +1,36 @@
# Linux+ 1.6.1: Archives and compression
tar archives, restoring one file, gzip, bzip2 and xz side by side, reading a
compressed log, and cpio.
## Before you start
The lab clears away the files the tasks create in `/tmp`, if they're there.
Ubuntu empties `/tmp` at boot, so do the tasks without a reboot, and run
`le-lab check` before the chapter's clean-up step (`le-lab reset` cleans up
too). Tasks 2 and 3 ask for findings: write them to the files in
`/root/lab-answers/` that they name.
## Tasks
1. Archive `/etc` into `/tmp/etc.tar.gz` with gzip, list it to find
`sshd_config`, and restore just `etc/ssh/sshd_config` from it into
`/tmp/restore`.
2. Archive `/etc` without compression into `/tmp/etc.tar`. Copy it to
`/tmp/t-gzip.tar`, `/tmp/t-bzip2.tar` and `/tmp/t-xz.tar`, and compress
each with the tool it's named after, keeping the copies. Write the name
of the tool that made the smallest file to `/root/lab-answers/2`.
3. Save the last 500 lines of the journal, compressed with gzip, as
`/tmp/j.gz`. Without unpacking it, count the lines that mention `error`
in any case, and write the count to `/root/lab-answers/3`.
4. From `/etc`, make a cpio archive of the files in `ssh` as
`/tmp/ssh.cpio`, and unpack it into `/tmp/c`.
## Hints
- `tar -czf`, `tar -tzf`, `tar -xzf ARCHIVE -C DIR MEMBER`.
- `gzip -k`, `bzip2 -k`, `xz -k`; `ls -lh /tmp/t-*`. bzip2 may need
installing. The tools skip a file whose name already ends in their own
suffix (`.gz`, `.bz2`, `.xz`).
- `journalctl -n 500 --no-pager | gzip > ...`; `zgrep -c -i`, `zless`.
- `cd /etc && find ssh -type f | cpio -o > ...`; `cpio -idv < ...`.
@@ -0,0 +1,23 @@
smallest=$(ls -S /tmp/t-gzip.tar.gz /tmp/t-bzip2.tar.bz2 /tmp/t-xz.tar.xz 2>/dev/null | tail -n1 | sed 's|^/tmp/t-||; s|\..*||')
task 1 "/etc in /tmp/etc.tar.gz; sshd_config restored into /tmp/restore"
need "/tmp/etc.tar.gz is a gzip-compressed tar archive" 'gzip -t /tmp/etc.tar.gz && tar -tzf /tmp/etc.tar.gz >/dev/null'
need "it has etc/ssh/sshd_config" 'tar -tzf /tmp/etc.tar.gz | grep -qx etc/ssh/sshd_config'
need "/tmp/restore/etc/ssh/sshd_config matches /etc/ssh/sshd_config" 'cmp -s /tmp/restore/etc/ssh/sshd_config /etc/ssh/sshd_config'
task 2 "gzip, bzip2 and xz compared on /tmp/etc.tar"
need "/tmp/etc.tar is a tar archive of /etc" 'tar -tf /tmp/etc.tar | grep -qx etc/ssh/sshd_config'
need "/tmp/t-gzip.tar.gz is valid gzip" 'gzip -t /tmp/t-gzip.tar.gz'
need "/tmp/t-bzip2.tar.bz2 is valid bzip2" 'bzip2 -t /tmp/t-bzip2.tar.bz2'
need "/tmp/t-xz.tar.xz is valid xz" 'xz -t /tmp/t-xz.tar.xz'
need "the uncompressed copies are kept (-k)" '[ -f /tmp/t-gzip.tar ] && [ -f /tmp/t-bzip2.tar ] && [ -f /tmp/t-xz.tar ]'
need "/root/lab-answers/2 names the tool with the smallest file${smallest:+, $smallest}" '[ -n "$smallest" ] && [ "$(answer 2)" = "$smallest" ]'
task 3 "The journal in /tmp/j.gz, its error lines counted"
need "/tmp/j.gz is valid gzip" 'gzip -t /tmp/j.gz'
need "it holds lines from the journal" '[ "$(zcat /tmp/j.gz | wc -l)" -ge 50 ]'
need "/root/lab-answers/3 has the count of lines with error" '[ "$(answer 3 | tr -dc 0-9)" = "$(zgrep -c -i error /tmp/j.gz)" ]'
task 4 "ssh in /tmp/ssh.cpio, unpacked in /tmp/c"
need "/tmp/ssh.cpio is a cpio archive with ssh/sshd_config" 'cpio -it </tmp/ssh.cpio 2>/dev/null | grep -qx ssh/sshd_config'
need "/tmp/c/ssh/sshd_config matches /etc/ssh/sshd_config" 'cmp -s /tmp/c/ssh/sshd_config /etc/ssh/sshd_config'
@@ -0,0 +1,3 @@
TITLE="1.6.1 Archives and compression"
CHAPTER=/linux-plus/1-system-management/1-6-1-archives-and-compression/
SYSTEMS="rhel debian"
@@ -0,0 +1,2 @@
rm -rf /tmp/etc.tar /tmp/etc.tar.gz /tmp/t-gzip.tar* /tmp/t-bzip2.tar* /tmp/t-xz.tar* /tmp/j.gz /tmp/ssh.cpio /tmp/restore /tmp/c
true
@@ -0,0 +1 @@
rm -rf /tmp/etc.tar /tmp/etc.tar.gz /tmp/t-gzip.tar* /tmp/t-bzip2.tar* /tmp/t-xz.tar* /tmp/j.gz /tmp/ssh.cpio /tmp/restore /tmp/c
@@ -0,0 +1,36 @@
# Linux+ 1.6.2: Copies, syncs and disk images
Mirroring a directory with rsync, pulling files from another machine over
SSH, and imaging a disk with dd.
## Before you start
The peer, at `10.99.0.2` (name `peer`), stands in for the other VM. It runs
its own sshd and shares this VM's files and users, so its `/etc` is this
VM's `/etc`. The lab creates user `alex` (password `Lab-Pass-1`) and adds a
small 64 MiB spare disk, `/dev/loop40` (the chapter's `vdb`), with a
filesystem on it. Several files go in `/tmp`, which Ubuntu empties at boot,
so do the tasks without a reboot and run `le-lab check` before you clean up.
## Tasks
1. Copy `/etc/ssh` into `/tmp/src` and mirror `/tmp/src/` to `/tmp/dst/`
with rsync. Add a file `/tmp/src/ssh/new.txt` and delete
`/tmp/src/ssh/moduli`, try a dry run, then mirror again so that
`/tmp/dst` matches. Finally, copy `/tmp/src` (no trailing slash) into
`/tmp/dst2/`, and see where it lands.
2. As `alex`, set up SSH keys so that alex can log in to the peer
(`[email protected]`) without a password, then pull a copy of the peer's
`/etc/` into `/tmp/other-etc/` with rsync. Files only root can read are
skipped with "Permission denied".
3. Image `/dev/loop40` into `/tmp/vdb.img` with dd and compare the two with
`cmp`. Keep the image until you've run `le-lab check`.
## Hints
- `rsync -av SRC/ DST/`, `rsync -avn --delete`, `rsync -a SRC DST/`.
- `sudo -iu alex`, `ssh-keygen`. The peer shares alex's home directory, so
adding the public key to `~/.ssh/authorized_keys` is enough
(`ssh-copy-id` needs a password login, which may be turned off).
`rsync -a [email protected]:/etc/ /tmp/other-etc/`.
- `dd if=DISK of=IMAGE bs=4M status=progress`, `cmp DISK IMAGE && echo identical`.
@@ -0,0 +1,16 @@
task 1 "/tmp/dst mirrors /tmp/src; /tmp/src copied into /tmp/dst2"
need "/tmp/dst/ssh/sshd_config is there" '[ -f /tmp/dst/ssh/sshd_config ]'
need "/tmp/dst/ssh/new.txt is there" '[ -f /tmp/dst/ssh/new.txt ]'
need "/tmp/dst/ssh/moduli is gone (--delete)" '[ ! -e /tmp/dst/ssh/moduli ] && [ ! -e /tmp/src/ssh/moduli ]'
need "/tmp/dst matches /tmp/src" '[ -z "$(rsync -an --delete --out-format=%n /tmp/src/ /tmp/dst/ | grep -v "/$")" ]'
need "/tmp/dst2 has the directory src in it" '[ -d /tmp/dst2/src/ssh ]'
task 2 "The peer's /etc pulled over SSH into /tmp/other-etc as alex"
need "alex has an SSH key the peer accepts" '
runuser -u alex -- ssh -n -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=8 [email protected] true'
need "/tmp/other-etc has the peer's /etc files" '[ -f /tmp/other-etc/passwd ] && [ -d /tmp/other-etc/ssh ]'
need "alex made the copy" '[ "$(stat -c %U /tmp/other-etc/passwd)" = alex ]'
need "files only root can read were skipped" '[ ! -e /tmp/other-etc/shadow ]'
task 3 "/dev/loop40 imaged into /tmp/vdb.img"
need "/tmp/vdb.img is identical to /dev/loop40" '[ -f /tmp/vdb.img ] && cmp -s /dev/loop40 /tmp/vdb.img'
@@ -0,0 +1,3 @@
TITLE="1.6.2 Copies, syncs and disk images"
CHAPTER=/linux-plus/1-system-management/1-6-2-copying-and-imaging/
SYSTEMS="rhel debian"
@@ -0,0 +1,5 @@
systemctl disable --now --quiet le-lab-peer-sshd.service 2>/dev/null
rm -f /etc/systemd/system/le-lab-peer-sshd.service
systemctl daemon-reload
rm -rf /tmp/src /tmp/dst /tmp/dst2 /tmp/other-etc /tmp/vdb.img
true
@@ -0,0 +1,28 @@
command -v rsync >/dev/null || refuse "rsync isn't installed; install it first (the chapter uses it on both machines)"
[ -x /usr/sbin/sshd ] || refuse "sshd isn't installed; install openssh-server first"
rm -rf /tmp/src /tmp/dst /tmp/dst2 /tmp/other-etc /tmp/vdb.img
user_add alex -s /bin/bash
peer_up
# The peer's sshd, on the peer's own address.
cat >/etc/systemd/system/le-lab-peer-sshd.service <<'X'
[Unit]
Description=Practice lab sshd on the peer
Requires=le-lab-peer.service
After=le-lab-peer.service network.target
[Service]
NetworkNamespacePath=/run/netns/lepeer
ExecStartPre=/bin/mkdir -p /run/sshd
ExecStart=/usr/sbin/sshd -D -o ListenAddress=10.99.0.2:22 -o PidFile=none
KillMode=process
Restart=on-failure
[Install]
WantedBy=multi-user.target
X
systemctl daemon-reload
systemctl enable --now --quiet le-lab-peer-sshd.service
# A small disk with something on it to image.
disk_add 0 64M >/dev/null
mkfs.ext4 -q -L labdata /dev/loop40
m=$(mktemp -d); mount /dev/loop40 "$m"; cp -r /etc/ssh "$m/"; umount "$m"; rmdir "$m"
@@ -0,0 +1,35 @@
# Linux+ 1.7.1: KVM, QEMU and virtual disks
Getting a host ready for KVM, and working with disk images: qcow2 and raw,
resizing, converting, and a backing file.
## Before you start
Task 1 is best done on a VM with **nested virtualization** (the CPU flag
`vmx` or `svm` inside it), but nothing here runs a VM, so the lab works
without it too: `virt-host-validate` then reports what's missing. The
images go in `/tmp`, which Ubuntu empties at boot, so do task 2 without a
reboot. Task 3 asks for a finding: write it to `/root/lab-answers/3`.
Reset removes the packages task 1 installs, if they weren't there before.
## Tasks
1. Check the host for KVM support, install QEMU, `qemu-img`, libvirt and
`virt-install`, and run `virt-host-validate qemu`.
2. Create a 5 GiB qcow2 image `/tmp/a.qcow2` and grow it by 1 GiB. Convert
it to a raw image, `/tmp/a.raw`, and compare the space each really
uses. Then create `/tmp/b.qcow2`, a qcow2 image with `/tmp/a.qcow2` as
its backing file, and look at the backing chain.
3. Check whether this VM uses VirtIO devices. Write the driver of its main
network interface, as `ethtool -i` reports it, to `/root/lab-answers/3`.
## Hints
- `grep -cE 'vmx|svm' /proc/cpuinfo`, `lsmod | grep kvm`, `ls -l /dev/kvm`.
- RHEL family: `qemu-kvm qemu-img libvirt virt-install`. Ubuntu:
`qemu-system-x86 qemu-utils libvirt-daemon-system virtinst`.
- `qemu-img create -f qcow2`, `qemu-img info`, `qemu-img resize +1G`,
`qemu-img convert -f qcow2 -O raw`, `ls -ls`,
`qemu-img create -f qcow2 -F qcow2 -b BACKING NEW`,
`qemu-img info --backing-chain`.
- `lspci | grep -i virtio`; `ip route` shows the main interface.
@@ -0,0 +1,23 @@
IF=$(ip -4 route show default | awk '{for (i = 1; i < NF; i++) if ($i == "dev") { print $(i + 1); exit }}')
drv=$(basename "$(readlink -f "/sys/class/net/$IF/device/driver" 2>/dev/null)" 2>/dev/null)
command -v ethtool >/dev/null && drv=$(ethtool -i "$IF" 2>/dev/null | awk '$1 == "driver:" {print $2}')
info() { qemu-img info -U --output=json "$1" 2>/dev/null; }
field() { info "$1" | python3 -c 'import json,sys; v = json.load(sys.stdin).get(sys.argv[1]); print("" if v is None else v)' "$2" 2>/dev/null; }
task 1 "QEMU, qemu-img, libvirt and virt-install installed"
need "qemu-img is installed" 'command -v qemu-img'
need "a QEMU system emulator is installed" 'ls /usr/libexec/qemu-kvm /usr/bin/qemu-system-x86_64 2>/dev/null | grep -q .'
need "libvirt is installed (virsh and the QEMU driver)" 'command -v virsh && ls /usr/sbin/virtqemud /usr/sbin/libvirtd 2>/dev/null | grep -q .'
need "virt-install is installed" 'command -v virt-install'
need "virt-host-validate is installed" 'command -v virt-host-validate'
task 2 "a.qcow2 grown to 6 GiB, a.raw converted from it, b.qcow2 backed by it"
need "/tmp/a.qcow2 is a qcow2 image" '[ "$(field /tmp/a.qcow2 format)" = qcow2 ]'
need "/tmp/a.qcow2 is 6 GiB (5 + 1)" '[ "$(field /tmp/a.qcow2 virtual-size)" = 6442450944 ]'
need "/tmp/a.raw is a raw image of 6 GiB" '[ "$(field /tmp/a.raw format)" = raw ] && [ "$(stat -c %s /tmp/a.raw)" = 6442450944 ]'
need "/tmp/b.qcow2 is a qcow2 image" '[ "$(field /tmp/b.qcow2 format)" = qcow2 ]'
need "/tmp/b.qcow2's backing file is /tmp/a.qcow2 (qcow2)" '
[ "$(readlink -f "$(field /tmp/b.qcow2 full-backing-filename)")" = /tmp/a.qcow2 ] && [ "$(field /tmp/b.qcow2 backing-filename-format)" = qcow2 ]'
task 3 "The main network interface's driver"
need "/root/lab-answers/3 has the driver of $IF${drv:+, $drv}" '[ -n "$drv" ] && [ "$(answer 3)" = "$drv" ]'
@@ -0,0 +1,3 @@
TITLE="1.7.1 KVM, QEMU and virtual disks"
CHAPTER=/linux-plus/1-system-management/1-7-1-kvm-and-qemu/
SYSTEMS="rhel debian"
@@ -0,0 +1,12 @@
rm -f /tmp/a.qcow2 /tmp/a.raw /tmp/b.qcow2
true
# The packages task 1 installed, if they weren't there before the lab.
if [ -s "$LAB_STATE/not-installed" ]; then
say "Removing the packages task 1 installed..."
if is_rhel; then
dnf -y -q remove $(cat "$LAB_STATE/not-installed") >/dev/null 2>&1
else
DEBIAN_FRONTEND=noninteractive apt-get -y -qq --autoremove purge $(cat "$LAB_STATE/not-installed") >/dev/null 2>&1
fi
fi
true
@@ -0,0 +1,6 @@
grep -qE 'vmx|svm' /proc/cpuinfo || warn "This VM has no nested virtualization (no vmx or svm CPU flag); virt-host-validate will say so."
rm -f /tmp/a.qcow2 /tmp/a.raw /tmp/b.qcow2
# Reset takes out again what task 1 installs, so that the lab starts over.
for p in $(pick "qemu-kvm qemu-img libvirt virt-install" "qemu-system-x86 qemu-utils libvirt-daemon-system virtinst"); do
pkg_installed "$p" || printf '%s\n' "$p" >>"$LAB_STATE/not-installed"
done
@@ -0,0 +1,45 @@
# Linux+ 1.7.2: Managing virtual machines
Creating a VM from a cloud image with virt-install, suspending, resizing
and shutting it down, and a snapshot to undo a mistake.
## Before you start
The VM you run this lab on needs **nested virtualization** (the CPU flag
`vmx` or `svm` inside it) and about 6 GiB of memory, plus internet access.
The lab installs QEMU, libvirt and virt-install (the KVM host from chapter
1.7.1) and downloads a small cloud image, CirrOS, to
`/var/lib/le-lab/images/cirros.img`. Use it where the chapter downloads a
Rocky Linux or Ubuntu image (or download one of those yourself). Its user
is `cirros`, password `gocubsgo`.
## Tasks
1. Create a VM named `lab1` from the cloud image: copy it to
`/var/lib/libvirt/images/lab1.qcow2`, grow that to 10 GiB, and import it
with virt-install, with 1 GiB of memory, one vCPU, a VirtIO disk, a
VirtIO network card on the `default` network, and a generated root
password from cloud-init. `virsh list --all` shows it, and
`virsh domifaddr lab1` shows its address once it has booted.
2. Suspend and resume `lab1`, give it 768 MiB of memory while it runs, then
shut it down. Not graded.
3. Start `lab1` again, snapshot it as `clean`, break something inside it
(log in on `virsh console lab1` and run `rm -rf /etc/ssh`; Ctrl+] leaves
the console), and revert to the snapshot.
When `le-lab check` passes, clean up as the chapter shows (task 4: delete
the snapshot, then destroy and undefine `lab1` with its storage), or leave
it to `le-lab reset`.
## Hints
- On the RHEL family, start the libvirt sockets first:
`systemctl enable --now virtqemud.socket virtnetworkd.socket virtstoraged.socket`.
- `qemu-img resize`; `virt-install --import --disk PATH,bus=virtio
--network network=default,model=virtio --os-variant detect=on,require=off
--cloud-init root-password-generate=on --noautoconsole`.
- `virsh suspend`, `virsh resume`, `virsh setmem lab1 768M --live`,
`virsh shutdown`, `virsh domstate`; CirrOS may ignore `virsh shutdown`
while it's still booting, and `virsh destroy` powers it off.
- `virsh snapshot-create-as lab1 clean`, `virsh snapshot-list lab1`,
`virsh snapshot-revert lab1 clean`.
@@ -0,0 +1,19 @@
export LIBVIRT_DEFAULT_URI=qemu:///system
IMG=/var/lib/libvirt/images/lab1.qcow2
xml() { virsh dumpxml --inactive lab1 2>/dev/null; }
task 1 "lab1 imported from lab1.qcow2 (10 GiB), 1 GiB, VirtIO disk and network"
need "virsh list --all works" 'virsh list --all'
need "VM lab1 exists" 'virsh dominfo lab1'
need "lab1's disk is $IMG" 'virsh domblklist lab1 --inactive --details | awk "\$2 == \"disk\" {print \$4}" | grep -qxF "$IMG"'
need "$IMG is 10 GiB" 'qemu-img info -U --output=json "$IMG" | grep -Eq "\"virtual-size\": 10737418240,"'
need "the disk is on the VirtIO bus" 'xml | grep -Eq "<target dev=.vd[a-z]. bus=.virtio./>"'
need "the network card is VirtIO, on the default network" 'xml | grep -q "<source network=.default." && xml | grep -q "<model type=.virtio./>"'
need "lab1 has 1 GiB of memory" 'xml | grep -Eq "<memory unit=.KiB.>1048576</memory>"'
need "lab1 has one vCPU" 'xml | grep -Eq "<vcpu[^>]*>1</vcpu>"'
task 2 "Suspend, resume, setmem and shut down" self
task 3 "A snapshot clean of lab1, reverted to"
need "lab1 has a snapshot named clean" 'virsh snapshot-list lab1 --name | grep -qx clean'
need "clean is lab1's current snapshot (reverted)" '[ "$(virsh snapshot-current lab1 --name 2>/dev/null)" = clean ]'
@@ -0,0 +1,3 @@
TITLE="1.7.2 Managing virtual machines"
CHAPTER=/linux-plus/1-system-management/1-7-2-managing-vms/
SYSTEMS="rhel debian"
@@ -0,0 +1,12 @@
if command -v virsh >/dev/null; then
export LIBVIRT_DEFAULT_URI=qemu:///system
if virsh dominfo lab1 >/dev/null 2>&1; then
virsh destroy lab1 >/dev/null 2>&1
for s in $(virsh snapshot-list lab1 --name 2>/dev/null); do virsh snapshot-delete lab1 "$s" >/dev/null 2>&1; done
virsh undefine lab1 --remove-all-storage --snapshots-metadata --nvram >/dev/null 2>&1 ||
virsh undefine lab1 --snapshots-metadata >/dev/null 2>&1
fi
rm -f /var/lib/libvirt/images/lab1.qcow2
fi
rm -rf /var/lib/le-lab/images
true
Loaded 100 of 781 files, more files were not shown because too many files have changed in this diff. Show more