diff --git a/scripts/classroom-migrate b/scripts/classroom-migrate index 733728d..1b8b979 100755 --- a/scripts/classroom-migrate +++ b/scripts/classroom-migrate @@ -25,6 +25,15 @@ set -uo pipefail CLASSROOM_USER=classroom DEVICE_CONFIG=/etc/ekklesia-classroom/device.conf +# The classroom's own Python, wherever this is being run from: the package +# path first, and the checkout next to this script when there is no +# package — which is how the tests reach it. +LIB_DIR=/usr/local/lib/ekklesia-classroom + +if [[ ! -r "$LIB_DIR/classroom_config.py" ]]; then + LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd || true)" +fi + # The shells this appliance replaces. There is exactly one desktop stack # now — SDDM, KWin, plasmashell — and every one of these is a component of # a stack that is gone. @@ -260,6 +269,39 @@ if changed: PY fi +# --------------------------------------------- and then the shared defaults + +# Everything above is this package's own business: settings to remove, a +# channel to move, a broker to point at. What it deliberately does not do +# is know which settings the classroom has gained since a room was +# installed. That list lives in classroom_config.py, in one place, and +# ensure_device_config appends exactly the ones a file is missing and +# nothing else — so a room upgraded from any age ends up with all of them, +# whether it arrived here through the package's postinst or through +# somebody running this command by hand. Repeating the list here would +# mean two places to forget. +# +# It runs last on purpose. The migration above rewrites lines; this only +# ever appends, so appending afterwards cannot be undone by it. +# +# Outside the guard the migration is inside, because this also writes a +# device.conf for a machine that has none. +if [[ -n "$LIB_DIR" && -r "$LIB_DIR/classroom_config.py" ]]; then + if ! PYTHONPATH="$LIB_DIR" python3 - "$DEVICE_CONFIG" <<'SHARED' +import sys + +from classroom_config import ensure_device_config + +ensure_device_config(sys.argv[1]) +SHARED + then + say "WARNING: could not add this release's settings to device.conf" + fi +else + say "WARNING: classroom_config.py is missing, so device.conf may be short" + say " of settings this release added" +fi + # ------------------------------------------------------- the old session # Plasma reads the classroom's settings out of a directory that has to be diff --git a/tests/test-classroom-migrate-config.sh b/tests/test-classroom-migrate-config.sh index 421c748..0563fa4 100755 --- a/tests/test-classroom-migrate-config.sh +++ b/tests/test-classroom-migrate-config.sh @@ -20,6 +20,13 @@ import tempfile from pathlib import Path root = Path(sys.argv[1]) +sys.path.insert(0, str(root / "lib")) + +from classroom_config import ( # noqa: E402 + DEVICE_CONFIG_SECTIONS, + ensure_device_config, +) + script = (root / "scripts/classroom-migrate").read_text() block = re.search(r'python3 - "\$DEVICE_CONFIG" <<\'PY\'\n(.*?)\nPY\n', @@ -46,8 +53,12 @@ if block is None: migration = block.group(1) -def migrate(text): - """Run the real migration over a device.conf and return it.""" +def migrate(text, shared=False): + """Run the real migration over a device.conf and return it. + + With shared=True, the shared helper runs afterwards as well, which is + the whole sequence classroom-migrate performs. + """ with tempfile.TemporaryDirectory() as directory: source = Path(directory) / "migrate.py" source.write_text(migration) @@ -59,6 +70,9 @@ def migrate(text): capture_output=True, text=True, check=False, ) + if shared: + ensure_device_config(config) + return config.read_text(), result.stdout + result.stderr @@ -147,6 +161,68 @@ once, _ = migrate(BEFORE) twice, _ = migrate(once) check("the second run is a no-op", once == twice) +print() +print("5. a room old enough to be missing settings this release has") + +# The migration knows nothing about which settings the classroom has +# gained since a room was installed, and must not: that list lives in +# classroom_config.py. What it has to do is hand the file to the shared +# helper afterwards, so a machine converges whether it arrived through the +# package's postinst or through somebody running the command by hand. +AGED = """# A room set up a long time ago. Keep this comment. +UPDATE_URL=https://updates.riverofglorymn.org +CHANNEL=a-channel-somebody-named +COMPOSITOR=hyprland +MQTT_ENABLED=false +MQTT_HOST= +MQTT_USERNAME=classroom +MQTT_PASSWORD=preserve-me +CUSTOM_SETTING=preserve-me-too +""" + +after, output = migrate(AGED, shared=True) + +missing = [key for key, _ in DEVICE_CONFIG_SECTIONS] +check("there are settings for this to be about", len(missing) >= 3) + +for key in ("LIVE_ENABLED", "AUDIO_OUTPUT", "UI_SCALE"): + check(f"{key} is appended", f"\n{key}=" in "\n" + after) + check(f"{key} exactly once", + len(re.findall(rf"^{key}=", after, re.M)) == 1) + +check("every setting this release knows about is present", + all(re.search(rf"^{key}=", after, re.M) for key in missing)) +check("the room's comment survives", + "# A room set up a long time ago. Keep this comment." in after) +check("the custom setting survives", "CUSTOM_SETTING=preserve-me-too" in after) +check("the password survives", "MQTT_PASSWORD=preserve-me" in after) +check("and is still never printed", "preserve-me" not in output) +check("the username survives", "MQTT_USERNAME=classroom" in after) +check("the deliberate channel is still deliberate", + "CHANNEL=a-channel-somebody-named" in after) +check("the migration still did its own work", + "COMPOSITOR" not in after + and "MQTT_ENABLED=true" in after + and "MQTT_HOST=10.40.0.2" in after) + +twice, _ = migrate(after, shared=True) +check("and running the whole thing again changes nothing", twice == after) + +print() +print("6. the defaults live in one place") + +check("the migration does not carry a copy of them", + not any(key in migration + for key, _ in DEVICE_CONFIG_SECTIONS + if key not in ("MQTT_ENABLED",))) +check("the script calls the shared helper", + "ensure_device_config" in script) +check("after its own migration, not before", + script.index("ensure_device_config") + > script.index("OBSOLETE_CHANNELS")) +check("with the file it was given", + "ensure_device_config(sys.argv[1])" in script) + print() print(f"{passed} passed, {failed} failed") sys.exit(1 if failed else 0) diff --git a/tests/test-deployment-support.sh b/tests/test-deployment-support.sh index d58f2b5..7a5c29c 100755 --- a/tests/test-deployment-support.sh +++ b/tests/test-deployment-support.sh @@ -211,6 +211,18 @@ package_copy = canary.index('cp "$package_path"') first_rsync = canary.index("rsync -av") check("Trixie validation runs before the repository copy", validation < package_copy) check("Trixie validation runs before any upload", validation < first_rsync) +# The gate checks convergence, not just that the package installs: an +# upgraded room ends on the v2 channel with every setting this release +# has, exactly once, and the command a technician runs is safe to run on +# a machine that is already converged. +check("the gate checks the upgraded channel", + "'^CHANNEL=classroom-v2$'" in validation_script) +check("and that every new setting arrived exactly once", + "for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE" in validation_script) +check("and that running the migration by hand is safe", + "classroom-migrate >/tmp/migrate-by-hand.log" in validation_script) +check("and that it never prints the broker password", + "! grep -q 'preserve-me' /tmp/migrate-by-hand.log" in validation_script) print() print("7. one scale, applied everywhere") diff --git a/tools/validate-trixie-release.sh b/tools/validate-trixie-release.sh index 164740e..ca09c68 100755 --- a/tools/validate-trixie-release.sh +++ b/tools/validate-trixie-release.sh @@ -141,16 +141,46 @@ timeout --signal=TERM --kill-after=5s 45s classroom-provision \ grep -q 'Elmer is already correctly configured' /tmp/provision-second.log if [[ -n "$previous" ]]; then - grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf + # 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 - [[ "$(grep -c '^LIVE_ENABLED=' /etc/ekklesia-classroom/device.conf)" -eq 1 ]] + + # And the one value the upgrade is supposed to change. canary is the + # sway line: a room that has just taken the Plasma package and is left + # pointed at it is handed the old desktop back by the next release + # there, which is the upgrade undoing itself. + grep -q '^CHANNEL=classroom-v2$' /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 - [[ "$(grep -c '^LIVE_ENABLED=' /etc/ekklesia-classroom/device.conf)" -eq 1 ]] + + # 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=classroom-v2$' /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."