Practice labs for RHCSA, LPIC-1 and Linux+ #2
No files matched your search
+10
-2
@@ -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).
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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 ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.3.2 Logical volume management"
|
||||
CHAPTER=/linux-plus/1-system-management/1-3-2-lvm/
|
||||
SYSTEMS="rhel debian"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.3.4 Mounting filesystems"
|
||||
CHAPTER=/linux-plus/1-system-management/1-3-4-mounting/
|
||||
SYSTEMS="rhel debian"
|
||||
@@ -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
|
||||
@@ -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)"
|
||||
@@ -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`.
|
||||
@@ -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 ]'
|
||||
@@ -0,0 +1,3 @@
|
||||
TITLE="1.3.5 Software RAID"
|
||||
CHAPTER=/linux-plus/1-system-management/1-3-5-raid/
|
||||
SYSTEMS="rhel debian"
|
||||
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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
Reference in new issue
Block a user