diff --git a/tools/lab/update-schedule-check b/tools/lab/update-schedule-check new file mode 100755 index 0000000..697ae0c --- /dev/null +++ b/tools/lab/update-schedule-check @@ -0,0 +1,162 @@ +#!/bin/bash +# When a classroom checks for updates, measured on the lab's own systemd. +# +# switched on at the time the timer fires once at its time, and not +# again that day +# switched off at the time the lab machine is really stopped across the +# time, then started: the missed check runs +# once after it boots, and does not loop +# +# A pair of test units stands in for ekklesia-classroom-update: the same +# [Timer] settings with the time moved to a minute or two from now (the +# real one would update the lab for real). The real timer's own settings, +# after installing the package, are the release gate's to check. +# +# And classroom-reboot-when-idle against real kernel input: a mouse made +# with /dev/uinput, moved and then left alone, with a harmless command in +# place of the reboot. +# +# tools/lab/update-schedule-check +set -uo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +NAME="${LAB_NAME:-classroom-v2-lab}" +LAB="$ROOT/tools/lab/lab" +IDLE="${IDLE_COMMAND:-/usr/local/bin/classroom-reboot-when-idle}" +LOG=/var/tmp/ekk-schedule-test.log + +lab() { docker exec -i "$NAME" bash -c "$1"; } + +passed=0 +failed=0 + +judge() { + if eval "$2"; then + printf ' \033[32mPASS\033[0m %s\n' "$1" + passed=$((passed + 1)) + else + printf ' \033[31mFAIL\033[0m %s\n' "$1" + failed=$((failed + 1)) + fi +} + +# A timer like the real one, due at HH:MM:SS local time, with DELAY spread. +install_test_timer() { # WHEN DELAY + lab "cat > /etc/systemd/system/ekk-schedule-test.service <<'UNIT' +[Unit] +Description=Stand-in for the update check + +[Service] +Type=oneshot +ExecStart=/bin/sh -c 'echo \"fired \$(date +%%s) boot \$(cut -d. -f1 /proc/uptime)\" >> $LOG' +UNIT +cat > /etc/systemd/system/ekk-schedule-test.timer </dev/null 2>&1" +} + +fired() { lab "grep -c fired $LOG 2>/dev/null || echo 0" | tail -1; } +local_in() { lab "date -d '+$1 seconds' +%H:%M:%S"; } # local time SECONDS from now + +echo "switched on at the time" +due=$(local_in 60) +install_test_timer "$due" 0 +judge "the stand-in is waiting for $due, local time" \ + "lab 'systemctl list-timers ekk-schedule-test.timer --no-legend | grep -q ekk-schedule-test'" +sleep 80 +judge "at $due it checked, once" '[[ $(fired) -eq 1 ]]' +sleep 60 +judge "and not again (a minute later, still once)" '[[ $(fired) -eq 1 ]]' +next=$(lab "systemctl show ekk-schedule-test.timer -p NextElapseUSecRealtime --value") +tomorrow=$(lab "date -d tomorrow +%Y-%m-%d") +judge "its next check is tomorrow at $due ($next)" '[[ "$next" == *"$tomorrow $due"* ]]' + +echo "switched off at the time" +due=$(local_in 90) +install_test_timer "$due" 60 +lab "systemctl start ekk-schedule-test.timer; sync" +# A stamp from a previous check, as every classroom has one: last checked +# yesterday. The catch-up is decided against it. +lab "touch -d yesterday /var/lib/systemd/timers/stamp-ekk-schedule-test.timer" +echo " powering the lab off before $due" +docker stop -t 20 "$NAME" >/dev/null +sleep 150 +echo " and on again, after it" +"$LAB" reboot >/dev/null 2>&1 +# When the lab machine really started (inside a container /proc/uptime is +# the host's, so this is asked of docker). +booted=$(date -d "$(docker inspect -f '{{.State.StartedAt}}' "$NAME")" +%s) +judge "while it was off, nothing ran" '[[ $(lab "grep -c fired $LOG 2>/dev/null || echo 0" | tail -1) -le 1 ]]' +for _ in $(seq 1 24); do [[ $(fired) -ge 1 ]] && break; sleep 5; done +judge "switched on after $due: the missed check ran after it booted ($(lab "tail -1 $LOG"))" '[[ $(fired) -eq 1 ]]' +after=$(( $(lab "awk '{print \$2}' $LOG | head -1") - booted )) +judge "$after s after it started: once it was up, within the minute of spread" '(( after >= 0 && after <= 150 ))' +sleep 90 +judge "and only once: no loop after booting (90 s later, still once)" '[[ $(fired) -eq 1 ]]' +lab "systemctl disable --now ekk-schedule-test.timer >/dev/null 2>&1; rm -f /etc/systemd/system/ekk-schedule-test.* $LOG /var/lib/systemd/timers/stamp-ekk-schedule-test.timer; systemctl daemon-reload" + +echo "reboot only when nobody is using it" +docker cp "$ROOT/scripts/classroom-reboot-when-idle" "$NAME:/opt/classroom-reboot-when-idle" >/dev/null +lab "chmod 755 /opt/classroom-reboot-when-idle; rm -f /tmp/rebooted" +[[ "$IDLE" == /usr/local/bin/* ]] && lab "test -x $IDLE" || IDLE=/opt/classroom-reboot-when-idle +cat > /tmp/update-schedule-mouse.py <<'PY' +"""A mouse made with /dev/uinput: real kernel input events. Moves for N s, then stays.""" +import fcntl, os, struct, sys, time +UI_SET_EVBIT, UI_SET_RELBIT, UI_SET_KEYBIT = 0x40045564, 0x40045566, 0x40045565 +UI_DEV_CREATE, UI_DEV_DESTROY = 0x5501, 0x5502 +EV_SYN, EV_KEY, EV_REL, REL_X, BTN_LEFT = 0, 1, 2, 0, 0x110 +fd = os.open("/dev/uinput", os.O_WRONLY | os.O_NONBLOCK) +for ioctl, value in ((UI_SET_EVBIT, EV_KEY), (UI_SET_KEYBIT, BTN_LEFT), (UI_SET_EVBIT, EV_REL), (UI_SET_RELBIT, REL_X)): + fcntl.ioctl(fd, ioctl, value) +os.write(fd, struct.pack("80sHHHHi", b"lab-classroom-mouse", 3, 1, 1, 1, 0) + b"\0" * (4 * 64 * 4)) +fcntl.ioctl(fd, UI_DEV_CREATE) +time.sleep(1) +def event(kind, code, value): + now = time.time() + os.write(fd, struct.pack("llHHi", int(now), int(now % 1 * 1e6), kind, code, value)) +end = time.monotonic() + float(sys.argv[1]) +while time.monotonic() < end: + event(EV_REL, REL_X, 1); event(EV_SYN, 0, 0) + time.sleep(2) +print("mouse left alone", flush=True) +time.sleep(float(sys.argv[2])) +fcntl.ioctl(fd, UI_DEV_DESTROY) +PY +docker cp /tmp/update-schedule-mouse.py "$NAME:/opt/update-schedule-mouse.py" >/dev/null +rm -f /tmp/update-schedule-mouse.py +docker exec -d "$NAME" python3 /opt/update-schedule-mouse.py 40 90 +sleep 2 +# The kernel makes the device; on a classroom machine devtmpfs gives it a +# node in /dev/input. The lab's /dev is a plain tmpfs, so the node is made +# here, from what sysfs says, as devtmpfs would. +lab 'for e in /sys/class/input/event*; do + [[ "$(cat $e/device/name)" == lab-classroom-mouse ]] || continue + IFS=: read -r major minor < $e/dev + mknod /dev/input/$(basename $e) c $major $minor 2>/dev/null + echo $(basename $e) > /tmp/lab-mouse-node + done' +sleep 1 +docker exec -d "$NAME" sh -c "$IDLE --idle-seconds 25 --check-seconds 2 --reboot-command touch /tmp/rebooted > /tmp/idle.log 2>&1" +sleep 35 +judge "the mouse moved every 2 s for 40 s: no reboot, against a 25 s limit" "! lab 'test -e /tmp/rebooted'" +judge "it was watching real devices ($(lab 'grep -o "watching [0-9]* input devices" /tmp/idle.log'))" \ + "lab 'grep -qE \"watching [1-9][0-9]* input devices\" /tmp/idle.log'" +sleep 40 +judge "left alone for longer than the limit: it rebooted" "lab 'test -e /tmp/rebooted'" +lab "pkill -f '[u]pdate-schedule-mouse'; rm -f /dev/input/\$(cat /tmp/lab-mouse-node 2>/dev/null) /tmp/lab-mouse-node /tmp/rebooted /opt/update-schedule-mouse.py /opt/classroom-reboot-when-idle" + +echo +echo "$passed passed, $failed failed" +[[ "$failed" -eq 0 ]]