Release 0.1.37 deployment prep

This commit is contained in:
ekklesia
2026-09-06 17:30:30 -05:00
parent 4862e29f57
commit 7f66228b9e
19 changed files with 866 additions and 79 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.36
0.1.37
@@ -14,6 +14,7 @@
"PasswordManagerEnabled": false,
"PasswordLeakDetectionEnabled": false,
"BackgroundModeEnabled": false,
"DisablePrintPreview": true,
"CookiesSessionOnlyForUrls": [
"[*.]wonderink.org"
+2 -3
View File
@@ -13,9 +13,8 @@ DVD_ENABLED=true
Then restart the session (reboot, or restart `classroom-dock`).
With `DVD_ENABLED=false`, or the key absent, the DVD button is not built
at all and no drive polling happens. Older endpoints get a commented
`DVD_ENABLED=false` appended on upgrade so the option is discoverable,
which leaves their behaviour unchanged.
at all and no drive polling happens. The LIVE placeholder is controlled
independently by `LIVE_ENABLED`; see [live.md](live.md).
## Behaviour
+19
View File
@@ -0,0 +1,19 @@
# Sanctuary LIVE dock capability
Sanctuary live viewing is not implemented. The existing placeholder can be
shown on development devices or hidden on ordinary classrooms with
`/etc/ekklesia-classroom/device.conf`:
```
LIVE_ENABLED=false
```
With `LIVE_ENABLED=false`, or when the key is absent, the LIVE button is not
built at all, so it leaves no empty space and starts no live-specific work.
With `LIVE_ENABLED=true`, the current LIVE icon and placeholder notification
appear unchanged. This flag does not enable playback or start a streaming
service.
Older endpoints receive one commented `LIVE_ENABLED=false` setting on
upgrade. Existing device configuration values and comments are preserved.
+42
View File
@@ -0,0 +1,42 @@
# Classroom networking
Ethernet remains the normal classroom connection. The package also installs
and enables NetworkManager so an administrator can add Wi-Fi when a room needs
it; there is no classroom Wi-Fi screen and no network name or password in the
package.
## Add Wi-Fi
From a terminal or SSH session, run:
```
sudo nmtui
```
Choose **Activate a connection**, select the church Wi-Fi network, enter its
password, and activate it. NetworkManager saves the connection as a system
connection and reconnects it automatically after reboot.
Useful checks:
```
nmcli device status
nmcli connection show
systemctl status NetworkManager
```
There is nothing to configure on a computer without Wi-Fi hardware.
NetworkManager starts normally, sees no Wi-Fi device, and Ethernet continues
to work.
## Existing Ethernet configuration
Do not remove or rewrite `/etc/network/interfaces` as part of this procedure.
Debian Trixie's NetworkManager configuration uses the `ifupdown` plugin with
`managed=false`, so any Ethernet interface declared there remains under
ifupdown's control. NetworkManager manages other interfaces, including Wi-Fi.
This preserves an existing static or ifupdown Ethernet setup.
The package disables `NetworkManager-wait-online.service`; no classroom
feature is allowed to hold boot waiting for Wi-Fi or Internet access. Saved
connections still reconnect automatically.
+77
View File
@@ -0,0 +1,77 @@
# Classroom printing
The package installs and enables CUPS, its client/filter stack, and Debian's
compressed Foomatic PPD database. Chrome is configured to open the system
print dialog, which shows the queues configured in CUPS.
The target Ricoh Aficio MP C3502 supports PCL 5c and PCL 6 as standard;
PostScript 3 is optional. Debian Trixie does not provide a model-specific
C3502 PPD in the installed driver set, so the classroom helper uses Trixie's
recommended generic color PCL 6/PCL XL driver. This works with the printer's
standard language and does not require a vendor binary.
## Configure the church color printer
Give the helper the printer's IP address or DNS name:
```
sudo classroom-printer-setup 10.20.30.40
```
This creates or updates one queue named `Classroom_Color`, points it directly
to `socket://10.20.30.40:9100`, enables it, accepts jobs, and makes it the
system default. The printer's address is never stored in the package.
An explicit URI is also accepted when deployment requires another supported
transport:
```
sudo classroom-printer-setup ipp://10.20.30.40/ipp/print
sudo classroom-printer-setup --no-default socket://10.20.30.40:9100
```
The command is idempotent: running it again updates the same queue. It does not
contact the printer while configuring CUPS, and an unreachable printer only
leaves jobs queued; it never affects boot.
Automatic remote queue browsing is disabled. `cups-browsed` is stopped and
disabled if another package installed it, so unrelated advertised label and
check-in printers are not turned into local classroom queues.
## Administration
List the available Debian drivers, including the selected PCL path:
```
lpinfo -m
lpinfo -m | grep -i -E 'Ricoh|PCL|PostScript|IPP Everywhere'
```
Show the classroom queue, its URI, and the system default:
```
lpstat -p Classroom_Color -v
lpstat -d
```
Set the default manually:
```
sudo lpadmin -d Classroom_Color
```
Print CUPS's test page:
```
lp -d Classroom_Color /usr/share/cups/data/testprint
```
Inspect or cancel queued jobs:
```
lpstat -o Classroom_Color
cancel -a Classroom_Color
```
The printer language choice is based on Ricoh's published
[MP C3002/C3502 specifications](https://assets.ricoh-usa.com/j2jqn9lauv41/1CzJla6qwny2d9oBVHHlID/5bfa366a01cf82162ea24287f99c8af9/aficiompc3002.pdf).
+10 -1
View File
@@ -51,8 +51,17 @@ classroom package and apply as soon as Chrome exists.
Only for rooms with `DVD_ENABLED=true`. See [dvd.md](dvd.md).
## Networking and printing
NetworkManager and CUPS are Debian package dependencies, not deferred network
provisioning steps. Their services are enabled by the package and neither
requires Internet access or hardware to be present.
- Wi-Fi administration: [networking.md](networking.md)
- Church color printer setup: [printing.md](printing.md)
## What provisioning does not do
It never upgrades or reconfigures anything already installed, and it
The boot provisioning command never upgrades or reconfigures anything already installed, and it
never touches a repository it did not add. Classroom software updates
are the OTA channel's job, not this.
+43
View File
@@ -19,6 +19,31 @@ before promoting to stable with `./update_stable.sh`.
- [ ] A machine with no network still boots to a working shell, and
provisioning succeeds at a later boot once it is online
## Networking
- [ ] `nmtui` is installed and `NetworkManager.service` is enabled
- [ ] An Ethernet interface declared in `/etc/network/interfaces` remains
managed by ifupdown and keeps its existing address
- [ ] `sudo nmtui` can save and activate a Wi-Fi connection
- [ ] The saved Wi-Fi connection reconnects after reboot
- [ ] A machine with no Wi-Fi adapter reaches the classroom normally without
an online-wait delay or NetworkManager failure
- [ ] Ethernet-only operation is unchanged
## Printing
- [ ] `cups.service` is enabled and `lpstat -r` reports that the scheduler runs
- [ ] `lpinfo -m` contains the generic color PCL 6/PCL XL driver documented in
[printing.md](printing.md)
- [ ] `sudo classroom-printer-setup <printer-ip>` creates only
`Classroom_Color`, enables it, and makes it the system default
- [ ] Repeating the helper updates the same queue without creating a duplicate
- [ ] Chrome opens the system print dialog and shows the configured queue
- [ ] Unrelated label/check-in printers are not automatically added
- [ ] The CUPS test page prints in color on the Ricoh MP C3502
- [ ] With the Ricoh powered off, the classroom still boots normally and a job
simply remains queued
## UI scale
UI_SCALE sets sway's output scale, so it scales the whole session.
@@ -169,6 +194,19 @@ With `DVD_ENABLED=true`:
- [ ] Commercial CSS disc plays without anyone having run codec setup
by hand
## Sanctuary LIVE capability
With `LIVE_ENABLED=false`:
- [ ] No LIVE button or spacing hole appears in the dock
- [ ] No live-specific service or process starts
With `LIVE_ENABLED=true`:
- [ ] LIVE appears after DVD when DVD is enabled, and after Browser otherwise
- [ ] Clicking it shows the existing placeholder notification
- [ ] No live playback is attempted
### DVD mode
- [ ] Pressing DVD covers the **whole** display immediately, dock and
@@ -206,3 +244,8 @@ With `DVD_ENABLED=true`:
- [ ] SHA-256 verification passes
- [ ] Install succeeds and the endpoint reboots
- [ ] Everything above still works after the update
- [ ] Upgrade from 0.1.36 preserves every existing `device.conf` value
- [ ] `LIVE_ENABLED=false` is appended exactly once on upgrade and remains
single after reinstall
- [ ] `tools/validate-trixie-release.sh` passes against the 0.1.37 package and
the 0.1.36 package
+105
View File
@@ -19,6 +19,70 @@ UI_SCALE_MIN = 0.8
UI_SCALE_MAX = 1.5
DEVICE_CONFIG_TEMPLATE = """# Ekklesia Classroom device configuration
UPDATE_URL=https://updates.riverofglorymn.org
CHANNEL=stable
# Set to true on classrooms that have a DVD drive.
DVD_ENABLED=false
# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
# implemented yet; this only controls whether the current placeholder appears.
LIVE_ENABLED=false
# Size of the dock, clock and notifications, 0.8 to 1.5.
# Larger rooms and larger TVs want a larger number.
UI_SCALE=1.0
# Church message broker. The room is this machine's hostname unless ROOM
# is set. Leave MQTT_ENABLED=false until a broker exists.
MQTT_ENABLED=false
MQTT_HOST=
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
"""
# Sections added after the first classroom release. Keeping them separate lets
# an upgrade make a setting discoverable without replacing anything an
# administrator has already put in device.conf.
DEVICE_CONFIG_SECTIONS = (
(
"DVD_ENABLED",
"""# Set to true on classrooms that have a DVD drive.
DVD_ENABLED=false
""",
),
(
"LIVE_ENABLED",
"""# Show the Sanctuary LIVE placeholder in the dock. Live viewing is not
# implemented yet; this only controls whether the current placeholder appears.
LIVE_ENABLED=false
""",
),
(
"UI_SCALE",
"""# Size of the dock, clock and notifications, 0.8 to 1.5.
# Larger rooms and larger TVs want a larger number.
UI_SCALE=1.0
""",
),
(
"MQTT_ENABLED",
"""# Church message broker. The room is this machine's hostname unless ROOM
# is set. Leave MQTT_ENABLED=false until a broker exists.
MQTT_ENABLED=false
MQTT_HOST=
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
""",
),
)
def read_device_config(path=None):
config = {}
path = Path(path) if path is not None else CONFIG_FILE
@@ -45,6 +109,47 @@ def enabled(config, key, default=False):
return config[key].lower() in TRUTHY
def dock_items(config):
"""Dock items in display order, with disabled capabilities omitted."""
items = ["browser"]
if enabled(config, "DVD_ENABLED"):
items.append("dvd")
if enabled(config, "LIVE_ENABLED"):
items.append("live")
return tuple(items)
def ensure_device_config(path=None):
"""Create device.conf or append any missing settings exactly once.
Existing bytes are otherwise left alone: per-room settings, comments and
even keys this release does not know about all survive an upgrade.
"""
path = Path(path) if path is not None else CONFIG_FILE
if not path.exists():
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(DEVICE_CONFIG_TEMPLATE)
return
text = path.read_text()
config = read_device_config(path)
additions = []
for key, section in DEVICE_CONFIG_SECTIONS:
if key not in config:
additions.append(section.rstrip("\n"))
if not additions:
return
separator = "" if not text or text.endswith("\n") else "\n"
path.write_text(text + separator + "\n\n".join(additions) + "\n")
def ui_scale(config):
"""UI_SCALE, clamped to something a classroom can actually use."""
raw = config.get("UI_SCALE", "")
+1 -1
View File
@@ -4,7 +4,7 @@ Section: misc
Priority: optional
Architecture: amd64
Maintainer: Ekklesia
Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups, python3-gi, python3-gi-cairo, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, adwaita-icon-theme, libnotify-bin, curl, gnupg, ca-certificates, python3-paho-mqtt
Depends: sway, swaybg, waybar, mako-notifier, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, cups, cups-client, cups-filters, foomatic-db-compressed-ppds, python3-gi, python3-gi-cairo, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, adwaita-icon-theme, libnotify-bin, curl, gnupg, ca-certificates, python3-paho-mqtt
Recommends: vlc
Description: Ekklesia managed classroom environment
A minimal Wayland classroom environment for Ekklesia-managed church computers.
+24 -55
View File
@@ -30,6 +30,7 @@ chmod 0755 /usr/local/bin/classroom-scale
chmod 0755 /usr/local/bin/classroom-shutdown
chmod 0755 /usr/local/bin/classroom-chrome-setup
chmod 0755 /usr/local/bin/classroom-provision
chmod 0755 /usr/local/bin/classroom-printer-setup
# Reading the optical drive needs group cdrom. Takes effect at the next
# login, which the OTA reboot provides.
@@ -37,62 +38,14 @@ if getent group cdrom >/dev/null 2>&1; then
adduser classroom cdrom >/dev/null 2>&1 || true
fi
if [ ! -f /etc/ekklesia-classroom/device.conf ]; then
cat > /etc/ekklesia-classroom/device.conf <<'CONFIG'
# Ekklesia Classroom device configuration
# Create device.conf on a new endpoint, or append only settings an older
# endpoint does not yet know. The helper preserves every existing byte apart
# from the append, so per-room choices always survive an upgrade.
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 - <<'PY'
from classroom_config import ensure_device_config
UPDATE_URL=https://updates.riverofglorymn.org
CHANNEL=stable
# Set to true on classrooms that have a DVD drive.
DVD_ENABLED=false
# Size of the dock, clock and notifications, 0.8 to 1.5.
# Larger rooms and larger TVs want a larger number.
UI_SCALE=1.0
# Church message broker. The room is this machine's hostname unless ROOM
# is set. Leave MQTT_ENABLED=false until a broker exists.
MQTT_ENABLED=false
MQTT_HOST=
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
CONFIG
fi
# Make the settings discoverable on classrooms installed before they
# existed. Both defaults are what those rooms already do, so nothing
# about their behaviour changes.
if ! grep -q '^DVD_ENABLED=' /etc/ekklesia-classroom/device.conf; then
cat >> /etc/ekklesia-classroom/device.conf <<'CONFIG'
# Set to true on classrooms that have a DVD drive.
DVD_ENABLED=false
CONFIG
fi
if ! grep -q '^UI_SCALE=' /etc/ekklesia-classroom/device.conf; then
cat >> /etc/ekklesia-classroom/device.conf <<'CONFIG'
# Size of the dock, clock and notifications, 0.8 to 1.5.
# Larger rooms and larger TVs want a larger number.
UI_SCALE=1.0
CONFIG
fi
if ! grep -q '^MQTT_ENABLED=' /etc/ekklesia-classroom/device.conf; then
cat >> /etc/ekklesia-classroom/device.conf <<'CONFIG'
# Church message broker. The room is this machine's hostname unless ROOM
# is set. Leave MQTT_ENABLED=false until a broker exists.
MQTT_ENABLED=false
MQTT_HOST=
MQTT_PORT=1883
MQTT_USERNAME=
MQTT_PASSWORD=
CONFIG
fi
ensure_device_config()
PY
# The broker password lives here, so the file stops being world readable.
# Everything that reads it runs as root or as the classroom user.
@@ -101,6 +54,22 @@ chmod 0640 /etc/ekklesia-classroom/device.conf
systemctl daemon-reload || true
# NetworkManager ships nmtui and deliberately leaves interfaces declared in
# /etc/network/interfaces under ifupdown's control (managed=false). This gives
# administrators saved, auto-reconnecting Wi-Fi without taking over an
# existing static Ethernet setup. No classroom service needs to hold boot for
# NetworkManager's online wait, so keep that waiter disabled.
systemctl enable NetworkManager.service || true
systemctl disable NetworkManager-wait-online.service || true
systemctl start NetworkManager.service || true
# CUPS is ready at boot, but no queue is created until an administrator runs
# classroom-printer-setup. cups-browsed is unnecessary here and would expose
# every advertised printer on the church network if installed independently.
systemctl enable cups.socket cups.service || true
systemctl disable --now cups-browsed.service || true
systemctl start cups.socket || true
systemctl enable ekklesia-classroom-update.timer || true
systemctl start ekklesia-classroom-update.timer || true
+5 -6
View File
@@ -18,7 +18,7 @@ sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
import classroom_style as style # noqa: E402
from classroom_config import ( # noqa: E402
SHELL_SCALE,
enabled,
dock_items,
read_device_config,
scaled,
)
@@ -311,6 +311,8 @@ class ClassroomShell(Gtk.Window):
)
dock.get_style_context().add_class("dock")
items = dock_items(self.config)
dock.pack_start(
self.make_button(
"browser",
@@ -322,11 +324,7 @@ class ClassroomShell(Gtk.Window):
0,
)
if enabled(
self.config,
"DVD_ENABLED",
False,
):
if "dvd" in items:
dock.pack_start(
self.make_button(
"dvd",
@@ -342,6 +340,7 @@ class ClassroomShell(Gtk.Window):
self.start_disc_watch()
if "live" in items:
dock.pack_start(
self.make_button(
"sanctuary",
+111
View File
@@ -0,0 +1,111 @@
#!/bin/bash
set -euo pipefail
# Configure the one explicitly managed classroom printer. The Ricoh Aficio
# MP C3502 supports PCL 6 as standard; Debian Trixie's compressed Foomatic
# database supplies this generic color PCL 6/PCL XL path without vendor code.
QUEUE="Classroom_Color"
DRIVER="foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd"
MAKE_MODEL="Ricoh Aficio MP C3502 (Generic PCL 6 Color)"
LOCATION="Ekklesia Classroom"
MAKE_DEFAULT=true
usage() {
cat <<'USAGE'
Usage: classroom-printer-setup [--no-default] PRINTER-IP-OR-URI
Examples:
sudo classroom-printer-setup 10.20.30.40
sudo classroom-printer-setup socket://printer.example.org:9100
An IP address or hostname uses JetDirect/AppSocket on port 9100. Explicit
socket://, ipp://, ipps:// and lpd:// URIs are also accepted.
USAGE
}
log() {
echo "[classroom-printer-setup] $1"
}
fail() {
log "ERROR: $1" >&2
exit 1
}
if [[ "${CLASSROOM_PRINTER_SETUP_TESTING:-false}" != "true" && $EUID -ne 0 ]]; then
fail "run this command as root: sudo classroom-printer-setup <printer-ip>"
fi
if [[ "${1:-}" == "--no-default" ]]; then
MAKE_DEFAULT=false
shift
fi
if [[ $# -ne 1 || "$1" == "-h" || "$1" == "--help" ]]; then
usage
[[ $# -eq 1 ]] && exit 0
exit 2
fi
address="$1"
case "$address" in
socket://*|ipp://*|ipps://*|lpd://*)
uri="$address"
;;
*://*)
fail "unsupported printer URI scheme: $address"
;;
*)
if [[ ! "$address" =~ ^[A-Za-z0-9._:-]+$ ]]; then
fail "printer address contains unsupported characters: $address"
fi
uri="socket://${address}:9100"
;;
esac
for command in lpadmin lpinfo lpstat cupsenable cupsaccept; do
command -v "$command" >/dev/null 2>&1 || \
fail "$command is missing; reinstall the ekklesia-classroom package"
done
if ! lpstat -r >/dev/null 2>&1; then
command -v systemctl >/dev/null 2>&1 && \
systemctl start cups.service >/dev/null 2>&1 || true
fi
lpstat -r >/dev/null 2>&1 || \
fail "CUPS is not running; check: systemctl status cups"
models="$(lpinfo -m)" || fail "CUPS could not list installed printer drivers"
if ! grep -q "^${DRIVER} " <<< "$models"; then
fail "the Debian generic color PCL 6 driver is not installed"
fi
log "configuring ${QUEUE} at ${uri}"
# Repeating this command updates the same named queue; it never creates a
# second destination. None of these operations connects to the printer.
lpadmin \
-p "$QUEUE" \
-v "$uri" \
-m "$DRIVER" \
-D "$MAKE_MODEL" \
-L "$LOCATION" || fail "CUPS could not configure ${QUEUE}"
cupsenable "$QUEUE" || fail "CUPS could not enable ${QUEUE}"
cupsaccept "$QUEUE" || fail "CUPS could not accept jobs for ${QUEUE}"
if [[ "$MAKE_DEFAULT" == true ]]; then
lpadmin -d "$QUEUE" || fail "CUPS could not make ${QUEUE} the default"
fi
log "${QUEUE} is configured, enabled and accepting jobs."
if [[ "$MAKE_DEFAULT" == true ]]; then
log "${QUEUE} is the system default printer."
fi
log "The printer may remain offline until it is reachable; this does not affect boot."
+104
View File
@@ -0,0 +1,104 @@
#!/bin/bash
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PYTHONPATH="$ROOT/lib" python3 - <<'PY'
import tempfile
from pathlib import Path
from classroom_config import dock_items, ensure_device_config, read_device_config
passed = failed = 0
def check(label, condition, detail=""):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
if detail:
print(f" {detail}")
failed += 1
print("1. dock capabilities")
cases = [
({}, ("browser",)),
({"DVD_ENABLED": "true"}, ("browser", "dvd")),
({"LIVE_ENABLED": "true"}, ("browser", "live")),
(
{"DVD_ENABLED": "true", "LIVE_ENABLED": "true"},
("browser", "dvd", "live"),
),
]
for config, expected in cases:
actual = dock_items(config)
check(f"{config or 'both disabled'} -> {expected}", actual == expected, actual)
check(
"false spellings do not leave placeholder items",
dock_items({"DVD_ENABLED": "false", "LIVE_ENABLED": "off"}) == ("browser",),
)
check(
"existing dock order is browser, DVD, LIVE",
dock_items({"DVD_ENABLED": "yes", "LIVE_ENABLED": "1"})
== ("browser", "dvd", "live"),
)
print()
print("2. fresh device configuration")
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "etc" / "device.conf"
ensure_device_config(path)
config = read_device_config(path)
check("fresh config is created", path.is_file())
check("DVD defaults off", config.get("DVD_ENABLED") == "false")
check("LIVE defaults off", config.get("LIVE_ENABLED") == "false")
check("MQTT defaults off", config.get("MQTT_ENABLED") == "false")
check("stable remains the fresh-install default", config.get("CHANNEL") == "stable")
print()
print("3. upgrade from 0.1.36")
old = """# endpoint configuration from 0.1.36
UPDATE_URL=https://updates.example.test
CHANNEL=canary
DVD_ENABLED=true
UI_SCALE=1.25
MQTT_ENABLED=true
MQTT_HOST=broker.example.test
MQTT_PORT=1884
MQTT_USERNAME=room
MQTT_PASSWORD=secret
CUSTOM_SETTING=keep-me
"""
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "device.conf"
path.write_text(old)
ensure_device_config(path)
first = path.read_text()
config = read_device_config(path)
check("old bytes remain at the start", first.startswith(old))
check("channel survives", config.get("CHANNEL") == "canary")
check("DVD choice survives", config.get("DVD_ENABLED") == "true")
check("UI scale survives", config.get("UI_SCALE") == "1.25")
check("MQTT host survives", config.get("MQTT_HOST") == "broker.example.test")
check("MQTT password survives", config.get("MQTT_PASSWORD") == "secret")
check("unknown settings survive", config.get("CUSTOM_SETTING") == "keep-me")
check("LIVE is appended once", first.count("LIVE_ENABLED=") == 1)
ensure_device_config(path)
second = path.read_text()
check("reinstall makes no further change", second == first)
check("LIVE still occurs once", second.count("LIVE_ENABLED=") == 1)
print()
print(f"passed: {passed} failed: {failed}")
raise SystemExit(1 if failed else 0)
PY
+116
View File
@@ -0,0 +1,116 @@
#!/bin/bash
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SCRIPT="$ROOT/scripts/classroom-printer-setup"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
passed=0
failed=0
check() {
local label="$1" expected="$2" actual="$3"
if [[ "$actual" == *"$expected"* ]]; then
echo " ok $label"
passed=$((passed + 1))
else
echo " FAIL $label"
echo " expected to contain: $expected"
echo " got: $actual"
failed=$((failed + 1))
fi
}
check_absent() {
local label="$1" unexpected="$2" actual="$3"
if [[ "$actual" != *"$unexpected"* ]]; then
echo " ok $label"
passed=$((passed + 1))
else
echo " FAIL $label"
echo " expected not to contain: $unexpected"
echo " got: $actual"
failed=$((failed + 1))
fi
}
mkdir -p "$WORK/bin"
for command in lpadmin cupsenable cupsaccept; do
cat > "$WORK/bin/$command" <<'SHIM'
#!/bin/bash
echo "$(basename "$0") $*" >> "$CUPS_LOG"
SHIM
done
cat > "$WORK/bin/lpstat" <<'SHIM'
#!/bin/bash
[[ "${FAKE_CUPS_RUNNING:-true}" == "true" ]]
SHIM
cat > "$WORK/bin/lpinfo" <<'SHIM'
#!/bin/bash
if [[ "${FAKE_DRIVER_PRESENT:-true}" == "true" ]]; then
echo "foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd Generic PCL 6/PCL XL Printer Foomatic/pxlcolor (recommended)"
else
echo "everywhere IPP Everywhere"
fi
SHIM
chmod +x "$WORK/bin"/*
export PATH="$WORK/bin:$PATH"
export CUPS_LOG="$WORK/cups.log"
export CLASSROOM_PRINTER_SETUP_TESTING=true
run() {
"$SCRIPT" "$@" 2>&1
}
echo "1. creates the explicit classroom queue"
: > "$CUPS_LOG"
out="$(run 10.20.30.40)"
log="$(cat "$CUPS_LOG")"
check "uses AppSocket by IP" "-v socket://10.20.30.40:9100" "$log"
check "uses Trixie's generic color PCL 6 driver" \
"Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd" "$log"
check "uses one stable queue name" "lpadmin -p Classroom_Color" "$log"
check "enables the queue" "cupsenable Classroom_Color" "$log"
check "accepts jobs" "cupsaccept Classroom_Color" "$log"
check "defaults the queue" "lpadmin -d Classroom_Color" "$log"
check "reports offline-safe behavior" "may remain offline" "$out"
echo
echo "2. repeated setup updates the same queue"
run 10.20.30.40 >/dev/null
configs="$(grep -c '^lpadmin -p Classroom_Color' "$CUPS_LOG")"
check "two runs configured the same queue twice" "2" "$configs"
check_absent "no generated queue suffix" "Classroom_Color_" "$(cat "$CUPS_LOG")"
echo
echo "3. explicit URIs and non-default setup"
: > "$CUPS_LOG"
run --no-default ipp://printer.example.test/ipp/print >/dev/null
log="$(cat "$CUPS_LOG")"
check "keeps an explicit IPP URI" "-v ipp://printer.example.test/ipp/print" "$log"
check_absent "--no-default omits the default change" "lpadmin -d" "$log"
echo
echo "4. errors are useful"
if out="$(run smb://printer/share)"; then
check "unsupported scheme fails" "failure" "unexpected success"
else
check "unsupported scheme is named" "unsupported printer URI scheme" "$out"
fi
export FAKE_DRIVER_PRESENT=false
if out="$(run 10.20.30.40)"; then
check "missing driver fails" "failure" "unexpected success"
else
check "missing driver is explained" "generic color PCL 6 driver is not installed" "$out"
fi
unset FAKE_DRIVER_PRESENT
echo
echo "passed: $passed failed: $failed"
[[ "$failed" -eq 0 ]]
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python3 - "$ROOT" <<'PY'
import json
import re
import sys
from pathlib import Path
root = Path(sys.argv[1])
control = (root / "packaging/debian/control.template").read_text()
postinst = (root / "packaging/debian/postinst").read_text()
provision = (root / "scripts/classroom-provision").read_text()
units = "\n".join(path.read_text() for path in (root / "systemd").rglob("*.service"))
canary = (root / "update_canary.sh").read_text()
policy = json.loads(
(root / "config/chrome/policies/managed/ekklesia-classroom.json").read_text()
)
passed = failed = 0
def check(label, condition):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
failed += 1
depends = re.search(r"^Depends: (.*)$", control, re.MULTILINE).group(1).split(", ")
print("1. Trixie package contract")
for package in (
"network-manager",
"cups",
"cups-client",
"cups-filters",
"foomatic-db-compressed-ppds",
):
check(f"depends on {package}", package in depends)
print()
print("2. networking service behavior")
check("NetworkManager is explicitly enabled", "systemctl enable NetworkManager.service || true" in postinst)
check("NetworkManager start is best effort", "systemctl start NetworkManager.service || true" in postinst)
check("online waiter is disabled", "systemctl disable NetworkManager-wait-online.service || true" in postinst)
check("no classroom NetworkManager config overrides ifupdown", not (root / "config/network").exists())
print()
print("3. printing service behavior")
check("CUPS service and socket are enabled", "systemctl enable cups.socket cups.service || true" in postinst)
check("CUPS start is best effort", "systemctl start cups.socket || true" in postinst)
check("automatic remote queue browsing is disabled", "systemctl disable --now cups-browsed.service || true" in postinst)
check("printer setup is never a boot service", "classroom-printer-setup" not in units)
check("printer setup is never provisioning", "classroom-printer-setup" not in provision)
check("Chrome uses the system print dialog", policy.get("DisablePrintPreview") is True)
print()
print("4. canary release gate")
validation = canary.index('"$ROOT/tools/validate-trixie-release.sh"')
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)
print()
print(f"passed: {passed} failed: {failed}")
raise SystemExit(1 if failed else 0)
PY
+4
View File
@@ -145,6 +145,10 @@ install -Dm755 \
"$ROOT/scripts/classroom-provision" \
"$STAGE/usr/local/bin/classroom-provision"
install -Dm755 \
"$ROOT/scripts/classroom-printer-setup" \
"$STAGE/usr/local/bin/classroom-printer-setup"
# Shared helpers imported by the classroom commands.
install -Dm644 \
"$ROOT/lib/classroom_config.py" \
+99
View File
@@ -0,0 +1,99 @@
#!/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
docker "${docker_args[@]}" debian:trixie-slim \
bash -s -- "${previous:+/packages/previous.deb}" <<'CONTAINER'
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
previous="${1:-}"
if [[ -n "$previous" ]]; then
apt-get install -y -qq "$previous" >/tmp/previous-install.log
cat > /etc/ekklesia-classroom/device.conf <<'CONFIG'
# Configuration carried forward from 0.1.36
UPDATE_URL=https://updates.riverofglorymn.org
CHANNEL=canary
DVD_ENABLED=true
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
apt-get install -y -qq /packages/candidate.deb >/tmp/candidate-install.log
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
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} "
classroom-printer-setup 192.0.2.10 >/tmp/printer-first.log
classroom-printer-setup 192.0.2.10 >/tmp/printer-second.log
lpstat -p Classroom_Color -d -v | grep -q 'socket://192.0.2.10:9100'
if [[ -n "$previous" ]]; then
grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf
grep -q '^DVD_ENABLED=true$' /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 ]]
apt-get install --reinstall -y -qq /packages/candidate.deb \
>/tmp/candidate-reinstall.log
[[ "$(grep -c '^LIVE_ENABLED=' /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
fi
echo "Trixie package, upgrade, networking, printing and reinstall checks passed."
CONTAINER
+16
View File
@@ -22,6 +22,22 @@ echo
"$ROOT/tools/build-release.sh" "$version"
# Validate the exact artifact that will be published. If a previous canary is
# available locally, exercise the real upgrade path as well as a fresh install.
validation_args=("$package_path")
if [[ -f "$ROOT/repo/canary/manifest.json" ]]; then
previous_package_name="$(python3 -c \
'import json; print(json.load(open("repo/canary/manifest.json"))["package"].rsplit("/", 1)[-1])')"
previous_package="$ROOT/repo/packages/$previous_package_name"
if [[ -f "$previous_package" && "$previous_package" != "$package_path" ]]; then
validation_args+=("$previous_package")
fi
fi
"$ROOT/tools/validate-trixie-release.sh" "${validation_args[@]}"
hash="$(sha256sum "$package_path" | awk '{print $1}')"
mkdir -p "$ROOT/repo/packages" "$ROOT/repo/canary"