The Bible study laptop had working internet and a panel that said there
was no connection. Both were true, and one thing caused both.
Measured on the machine:
nmcli device status wlp2s0 wifi unmanaged
nmcli networking connectivity none while curl fetched fine
nmcli device wifi list nothing, with the radio enabled
/run/network/ifstate wlp2s0=wlp2s0
a second wpa_supplicant, -i wlp2s0, with no -c config file
dhcpcd holding the lease
/etc/network/interfaces 0600 root:root
Debian's installer had been given the Wi-Fi, so it wrote it into
/etc/network/interfaces and ifupdown brings it up. NetworkManager ships
with plugins=ifupdown and [ifupdown] managed=false, so an interface named
in that file is UNMANAGED — which is why the applet had nothing to report
and why scanning found nothing. An unmanaged device is not a radio
NetworkManager will scan with. rfkill was ruled out separately: the radio
is enabled.
managed=false stays; it is what keeps a tower's static Ethernet where an
administrator put it. Only a wireless interface is moved.
classroom-wifi grows --check, --adopt (with --dry-run) and --rollback.
--adopt builds a NetworkManager SYSTEM connection from the SSID and key
already in that root-only file, so the password never leaves the machine
and never reaches a human or a log; it lands in a 0600 root file with
psk-flags=0, the same shape 0.2.4 established, and the polkit rules
protecting it are untouched.
Deliberately NOT run by the upgrade. It moves a machine's only link
between managers and the machine it could strand is a laptop in a locked
room, so classroom-migrate only looks and prints what it found. An
administrator runs it over SSH while watching.
Two things this learned from testing the rollback rather than from
reading the code, and both are why the rollback is worth testing:
* ifdown was missing in the lab, subprocess raised FileNotFoundError,
and the exception escaped AFTER the stanza was commented out. No
rollback ran. Every hand-over command now goes through a runner that
tolerates a missing binary, and the whole hand-over is inside a guard
that restores the file on any failure at all.
* it now refuses to start when ifdown or ifup are absent, because
without them the hand-over would be a one-way door.
Success also requires the interface itself to be carrying the connection,
not merely that the machine is online — a machine with a second link
would otherwise report a failed migration as a success.
Verified in the lab against real NetworkManager with a real ifupdown
file: --check and --dry-run leave it byte-identical, a failed --adopt
rolls back byte-identically, and --rollback restores a half-migrated
machine byte-identically and exits 0. The one thing no lab can do is the
hand-over succeeding, which needs the laptop's radio.
Co-Authored-By: Claude Opus 5 <[email protected]>
238 lines
9.3 KiB
Bash
Executable File
238 lines
9.3 KiB
Bash
Executable File
#!/bin/bash
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
|
echo "Usage: $0 CANDIDATE.deb [PREVIOUS.deb]" >&2
|
|
exit 2
|
|
fi
|
|
|
|
candidate="$(realpath "$1")"
|
|
previous=""
|
|
|
|
[[ -f "$candidate" ]] || { echo "Package not found: $candidate" >&2; exit 1; }
|
|
|
|
if [[ $# -eq 2 ]]; then
|
|
previous="$(realpath "$2")"
|
|
[[ -f "$previous" ]] || { echo "Package not found: $previous" >&2; exit 1; }
|
|
fi
|
|
|
|
echo "Validating $(basename "$candidate") on Debian Trixie"
|
|
|
|
docker_args=(
|
|
run --rm -i
|
|
-v "$candidate:/packages/candidate.deb:ro"
|
|
)
|
|
|
|
if [[ -n "$previous" ]]; then
|
|
docker_args+=( -v "$previous:/packages/previous.deb:ro" )
|
|
fi
|
|
|
|
timeout --signal=TERM --kill-after=15s 720s \
|
|
docker "${docker_args[@]}" debian:trixie-slim \
|
|
bash -s -- "${previous:+/packages/previous.deb}" <<'CONTAINER'
|
|
set -Eeuo pipefail
|
|
|
|
validation_failure() {
|
|
status=$?
|
|
echo "Validation failed at line ${BASH_LINENO[0]}: ${BASH_COMMAND}" >&2
|
|
|
|
if command -v lpstat >/dev/null 2>&1; then
|
|
lpstat -t >&2 || true
|
|
lpoptions -p Elmer -l >&2 || true
|
|
fi
|
|
|
|
[[ ! -f /tmp/provision-first.log ]] || cat /tmp/provision-first.log >&2
|
|
[[ ! -f /tmp/provision-second.log ]] || cat /tmp/provision-second.log >&2
|
|
exit "$status"
|
|
}
|
|
|
|
trap validation_failure ERR
|
|
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
|
|
# Package validation runs in a container with no init system. Do not let
|
|
# maintainer scripts try to start CUPS, Avahi or other irrelevant daemons;
|
|
# services are inspected and CUPS is started explicitly when the test needs it.
|
|
cat > /usr/sbin/policy-rc.d <<'POLICY'
|
|
#!/bin/sh
|
|
exit 101
|
|
POLICY
|
|
chmod 0755 /usr/sbin/policy-rc.d
|
|
|
|
run_logged() {
|
|
label="$1"
|
|
seconds="$2"
|
|
shift 2
|
|
|
|
if ! timeout --signal=TERM --kill-after=10s "${seconds}s" "$@" \
|
|
>"/tmp/${label}.log" 2>&1; then
|
|
cat "/tmp/${label}.log" >&2
|
|
echo "${label} exceeded ${seconds}s or failed" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
run_logged apt-update 120 apt-get update -qq
|
|
|
|
previous="${1:-}"
|
|
|
|
if [[ -n "$previous" ]]; then
|
|
run_logged previous-install 360 apt-get install -y -qq "$previous"
|
|
|
|
cat > /etc/ekklesia-classroom/device.conf <<'CONFIG'
|
|
# Configuration carried forward from 0.1.36
|
|
UPDATE_URL=https://updates.riverofglorymn.org
|
|
CHANNEL=canary
|
|
DVD_ENABLED=false
|
|
UI_SCALE=1.25
|
|
MQTT_ENABLED=true
|
|
MQTT_HOST=broker.example.test
|
|
MQTT_PORT=1884
|
|
MQTT_USERNAME=classroom
|
|
MQTT_PASSWORD=preserve-me
|
|
CUSTOM_SETTING=preserve-me-too
|
|
CONFIG
|
|
fi
|
|
|
|
run_logged candidate-install 360 apt-get install -y -qq /packages/candidate.deb
|
|
|
|
command -v nmtui >/dev/null
|
|
command -v lpadmin >/dev/null
|
|
command -v lpinfo >/dev/null
|
|
|
|
systemctl is-enabled NetworkManager.service >/dev/null
|
|
systemctl is-enabled cups.service >/dev/null
|
|
! systemctl is-enabled NetworkManager-wait-online.service >/dev/null 2>&1
|
|
|
|
# The audio policy reaches a session, and is ordered after the server it
|
|
# actually talks to. Ordering it only after pipewire and wireplumber is
|
|
# what let it race PipeWire's PulseAudio server at boot, fail its one
|
|
# startup attempt and then sit idle — the rooms were silent on 0.2.2 for
|
|
# exactly this reason, so the ordering is asserted rather than assumed.
|
|
audio_unit=/etc/systemd/user/ekklesia-classroom-audio.service
|
|
[[ -f "$audio_unit" ]]
|
|
grep -q 'pipewire-pulse' "$audio_unit"
|
|
[[ -L /etc/systemd/user/plasma-workspace.target.wants/ekklesia-classroom-audio.service ]]
|
|
command -v classroom-audio >/dev/null
|
|
command -v pactl >/dev/null
|
|
|
|
# The classroom account cannot be shown a saved Wi-Fi password. The rule
|
|
# has to be installed, owned by root — polkit silently ignores a rules
|
|
# file that is not — and it has to refuse the action Debian ships as
|
|
# "yes", which is the one a teacher would actually reach.
|
|
network_rules=/usr/share/polkit-1/rules.d/49-ekklesia-classroom-network.rules
|
|
[[ -f "$network_rules" ]]
|
|
[[ "$(stat -c %U:%G "$network_rules")" == "root:root" ]]
|
|
grep -q "settings.modify.own" "$network_rules"
|
|
grep -q "wifi.share.protected" "$network_rules"
|
|
# And it must NOT refuse the actions that keep the room on the network.
|
|
! grep -qE '"org\.freedesktop\.NetworkManager\.network-control"' "$network_rules"
|
|
grep -q "kcm_networkmanagement=false" /etc/ekklesia-classroom/xdg/kdeglobals
|
|
command -v classroom-wifi >/dev/null
|
|
|
|
# Adopting an installer-configured Wi-Fi is an administrator's command,
|
|
# never something an upgrade does on its own: it moves a machine's only
|
|
# link between managers, and the machine it would strand is a laptop in a
|
|
# locked room. The migration may only LOOK.
|
|
classroom-wifi --check 2>&1 | grep -q "no wireless interface"
|
|
classroom-wifi --adopt 2>&1 | grep -q "nothing to adopt"
|
|
! grep -qE '^[^#]*classroom-wifi --adopt' /usr/local/bin/classroom-migrate
|
|
grep -q "classroom-wifi --check" /usr/local/bin/classroom-migrate
|
|
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \
|
|
'import classroom_network; classroom_network.parse("")'
|
|
|
|
# The policy module and the daemon agree on what they import, on a machine
|
|
# rather than in a checkout.
|
|
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \
|
|
'import classroom_audio; classroom_audio.settled([], "auto", None)'
|
|
|
|
grep -q '^managed=false$' /etc/NetworkManager/NetworkManager.conf
|
|
|
|
# A normal Docker container has Ethernet and no Wi-Fi adapter. Reaching this
|
|
# point proves package configuration did not require Wi-Fi hardware.
|
|
! find /sys/class/net -mindepth 2 -maxdepth 2 -type d -name wireless | grep -q .
|
|
|
|
/usr/sbin/cupsd
|
|
sleep 1
|
|
|
|
driver='foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd'
|
|
lpinfo -m | grep -q "^${driver} "
|
|
|
|
# Simulate the queue an older release may have left behind, then run the real
|
|
# boot provisioning path. Chrome is stubbed as already installed and DVD is
|
|
# disabled so the test remains entirely local and deterministic.
|
|
lpadmin -p Classroom_Color -v socket://192.0.2.10:9100 -m "$driver"
|
|
install -m755 /bin/true /usr/local/bin/google-chrome
|
|
|
|
timeout --signal=TERM --kill-after=5s 45s classroom-provision \
|
|
>/tmp/provision-first.log
|
|
|
|
! lpstat -p Classroom_Color >/dev/null 2>&1
|
|
lpstat -p Elmer -d -v | grep -q 'socket://10.10.0.155:9100'
|
|
lpstat -l -p Elmer | grep -q 'Description: Elmer'
|
|
lpoptions -p Elmer -l | grep -Eq '^PrintoutMode/.*:.*\*Normal([[:space:]]|$)'
|
|
lpoptions -p Elmer -l | grep -Eq '^ColorModel/.*:.*\*Color([[:space:]]|$)'
|
|
[[ "$(lpstat -e)" == "Elmer" ]]
|
|
[[ "$(systemctl is-enabled avahi-daemon.service 2>/dev/null || true)" == "masked" ]]
|
|
[[ "$(systemctl is-enabled avahi-daemon.socket 2>/dev/null || true)" == "masked" ]]
|
|
|
|
timeout --signal=TERM --kill-after=5s 45s classroom-provision \
|
|
>/tmp/provision-second.log
|
|
grep -q 'Elmer is already correctly configured' /tmp/provision-second.log
|
|
|
|
if [[ -n "$previous" ]]; then
|
|
# The room's own values, untouched by the upgrade.
|
|
grep -q '^DVD_ENABLED=false$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^UI_SCALE=1.25$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
|
|
|
|
# The channel a room was put on deliberately survives the upgrade.
|
|
# canary is where the next development release goes, and the Plasma
|
|
# classroom is the stable line now, so nothing here has to move.
|
|
grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf
|
|
|
|
# And a room that was carried through the experiment on its own
|
|
# channel comes home, because that channel is the obsolete one now.
|
|
sed -i 's/^CHANNEL=.*/CHANNEL=classroom-v2/' /etc/ekklesia-classroom/device.conf
|
|
classroom-migrate >/tmp/migrate-from-v2.log 2>&1
|
|
grep -q '^CHANNEL=stable$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
|
|
sed -i 's/^CHANNEL=.*/CHANNEL=canary/' /etc/ekklesia-classroom/device.conf
|
|
|
|
# Every setting this release has that the room predates, appended
|
|
# exactly once. classroom-migrate hands device.conf to the shared
|
|
# helper after its own work for this, so that a machine converges
|
|
# whether it got here through this postinst or through somebody
|
|
# running the command by hand.
|
|
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
|
|
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
|
done
|
|
|
|
run_logged candidate-reinstall 360 \
|
|
apt-get install --reinstall -y -qq /packages/candidate.deb
|
|
|
|
# An install that changes nothing must change nothing.
|
|
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
|
|
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
|
done
|
|
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
|
|
grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf
|
|
|
|
# And by hand, on a machine that is already converged: the command a
|
|
# technician runs must be safe to run twice.
|
|
classroom-migrate >/tmp/migrate-by-hand.log 2>&1
|
|
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
|
|
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
|
|
done
|
|
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
|
|
! grep -q 'preserve-me' /tmp/migrate-by-hand.log
|
|
fi
|
|
|
|
echo "Trixie package, upgrade, bounded install, networking, Elmer printing and reinstall checks passed."
|
|
CONTAINER
|