From 17cb827be536c08024215c6df9653df34e7ac25b Mon Sep 17 00:00:00 2001 From: chrisfu Date: Fri, 13 Mar 2026 01:49:57 -0700 Subject: [PATCH] test(dashboard): add unit tests for systemd unit and script validation - Ensure dashboard service doesn't create an ordering cycle with `multi-user.target`. - Verify service safely attaches to a virtual terminal (VT) and passes VT to Xorg. - Ensure no `su` usage by default in the service. - Validate script behavior for passing VT and `-keeptty` options to Xorg. chore(dashboard): update systemd unit to avoid VT conflicts and improve logging - Remove `After` and `Wants` directives for `multi-user.target` to prevent conflicts. - Add `TTYPath`, `StandardInput`, and related options for VT attachment. - Ensure `ExecStopPost` restores getty service for the dashboard VT. fix(dashboard): improve Xorg resolution and clean up script logic - Dynamically determine Xorg executable location in `dashboard.sh`. - Pass VT, display, and `-keeptty` to Xorg to improve script reliability. fix(mariadb): correct exFAT mount options for external storage - Avoid `chown` failures by setting root ownership with group access for `mysql`. - Update external mount options to `uid=0,gid=mysql,umask=0002`. cleanup(mariadb): remove unnecessary owner/group permissions from migration marker task --- docs/MERLIN-MARIADB-EXTERNAL-TEMP.md | 5 ++ .../inventory/host_vars/merlin.prole.org.yml | 4 +- .../dashboard/templates/dashboard.service.j2 | 25 +++++++-- .../roles/mariadb_primary/tasks/main.yml | 3 -- tests/test_dashboard_systemd_unit.py | 54 +++++++++++++++++++ tools/dashboard.sh | 21 +++++++- 6 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 tests/test_dashboard_systemd_unit.py diff --git a/docs/MERLIN-MARIADB-EXTERNAL-TEMP.md b/docs/MERLIN-MARIADB-EXTERNAL-TEMP.md index afc2636..07cbffb 100644 --- a/docs/MERLIN-MARIADB-EXTERNAL-TEMP.md +++ b/docs/MERLIN-MARIADB-EXTERNAL-TEMP.md @@ -15,6 +15,11 @@ This is a temporary, host-specific setup for `merlin.prole.org` to move MariaDB - The disk is borrowed and formatted as `exFAT`. - Running MariaDB on `exFAT` is not ideal (permissions/ownership/ACLs/xattrs are limited), but this is an accepted short-term tradeoff until a better USB device is installed. +In particular: + +- `exFAT` does **not** support POSIX ownership, so `chown` will fail. +- The mount options should not force everything to `mysql:mysql` with a restrictive umask. For `merlin`, we mount as `uid=0,gid=mysql` with a permissive umask so the `mysql` group can still write. + ## Verify the mount On `merlin`: diff --git a/infrastructure/inventory/host_vars/merlin.prole.org.yml b/infrastructure/inventory/host_vars/merlin.prole.org.yml index c979f3f..618143a 100644 --- a/infrastructure/inventory/host_vars/merlin.prole.org.yml +++ b/infrastructure/inventory/host_vars/merlin.prole.org.yml @@ -18,7 +18,9 @@ mariadb_external_enabled: true mariadb_external_device: /dev/sda1 mariadb_external_mountpoint: /external mariadb_external_fstype: exfat -mariadb_external_mount_opts: "defaults,nofail,x-systemd.device-timeout=10,uid=mysql,gid=mysql,umask=0077" +# exFAT does not support POSIX ownership/permissions. +# Use a root-owned mount with group access for `mysql`, instead of forcing everything to `mysql:mysql`. +mariadb_external_mount_opts: "defaults,nofail,x-systemd.device-timeout=10,uid=0,gid=mysql,umask=0002" mariadb_external_src_datadir: /srv/mariadb/mariadb mariadb_external_dst_datadir: /external/mariadb diff --git a/infrastructure/roles/dashboard/templates/dashboard.service.j2 b/infrastructure/roles/dashboard/templates/dashboard.service.j2 index 6477407..4a60c87 100644 --- a/infrastructure/roles/dashboard/templates/dashboard.service.j2 +++ b/infrastructure/roles/dashboard/templates/dashboard.service.j2 @@ -1,18 +1,33 @@ [Unit] Description=Prole console dashboard (X + conky) -After=multi-user.target systemd-user-sessions.service -Wants=multi-user.target +After=systemd-user-sessions.service +Wants=systemd-user-sessions.service # If a graphical desktop is running, starting this service will stop it. Conflicts=graphical.target Before=graphical.target +# If a getty is running on the dashboard VT, stop it so Xorg can take over. +Conflicts=getty@tty{{ dashboard_vt }}.service + [Service] Type=simple -# Run the dashboard on the primary console (VT{{ dashboard_vt }}). -# openvt gives the process a controlling TTY so Xorg can take over the display. -ExecStart=/usr/bin/openvt -c {{ dashboard_vt }} -f -- /bin/su -l {{ dashboard_user }} -c {{ dashboard_script_dest | quote }} +# Attach the service to the dashboard VT so Xorg can take over the display. +TTYPath=/dev/tty{{ dashboard_vt }} +StandardInput=tty +StandardOutput=journal+console +StandardError=journal+console +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes + +Environment=DASHBOARD_VT={{ dashboard_vt }} + +ExecStart={{ dashboard_script_dest }} + +# When the dashboard stops, try to restore the login prompt on that VT. +ExecStopPost=-/usr/bin/systemctl --no-block start getty@tty{{ dashboard_vt }}.service Restart=always RestartSec=2 diff --git a/infrastructure/roles/mariadb_primary/tasks/main.yml b/infrastructure/roles/mariadb_primary/tasks/main.yml index 27f6838..e67d26e 100644 --- a/infrastructure/roles/mariadb_primary/tasks/main.yml +++ b/infrastructure/roles/mariadb_primary/tasks/main.yml @@ -142,9 +142,6 @@ - name: Write migration marker ansible.builtin.copy: dest: "{{ mariadb_external_marker }}" - owner: root - group: root - mode: "0644" content: | migrated_from={{ mariadb_external_src }} migrated_to={{ mariadb_external_dst }} diff --git a/tests/test_dashboard_systemd_unit.py b/tests/test_dashboard_systemd_unit.py new file mode 100644 index 0000000..23640e1 --- /dev/null +++ b/tests/test_dashboard_systemd_unit.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from pathlib import Path + + +def _project_root() -> Path: + # This test file lives in `/tests/`. + return Path(__file__).resolve().parents[1] + + +def _render_dashboard_service(*, vt: int = 1) -> str: + template_path = ( + _project_root() + / "infrastructure/roles/dashboard/templates/dashboard.service.j2" + ) + template = template_path.read_text(encoding="utf-8") + + # Keep this test dependency-free (no Jinja2 import required). + return ( + template.replace("{{ dashboard_vt }}", str(vt)) + .replace("{{ dashboard_script_dest }}", "/usr/local/bin/dashboard.sh") + .replace("{{ dashboard_user }}", "pi") + ) + + +def test_dashboard_service_is_safe_for_multi_user_target() -> None: + rendered = _render_dashboard_service(vt=1) + + # Avoid an ordering cycle with the target that wants the service. + assert "After=multi-user.target" not in rendered + assert "Wants=multi-user.target" not in rendered + assert "WantedBy=multi-user.target" in rendered + + +def test_dashboard_service_attaches_to_a_vt_and_passes_vt_to_script() -> None: + rendered = _render_dashboard_service(vt=1) + + assert "TTYPath=/dev/tty1" in rendered + assert "StandardInput=tty" in rendered + assert "Environment=DASHBOARD_VT=1" in rendered + + +def test_dashboard_service_does_not_use_su_by_default() -> None: + rendered = _render_dashboard_service(vt=1) + assert "su -l" not in rendered + + +def test_dashboard_script_passes_vt_and_keeptty_to_xorg() -> None: + script_path = _project_root() / "tools/dashboard.sh" + script = script_path.read_text(encoding="utf-8") + + assert "DASHBOARD_VT" in script + assert "vt${DASHBOARD_VT}" in script + assert "-keeptty" in script diff --git a/tools/dashboard.sh b/tools/dashboard.sh index c1bf3d9..05a315e 100644 --- a/tools/dashboard.sh +++ b/tools/dashboard.sh @@ -1,6 +1,23 @@ #!/usr/bin/env bash set -euo pipefail +DASHBOARD_DISPLAY="${DASHBOARD_DISPLAY:-:0}" +DASHBOARD_VT="${DASHBOARD_VT:-1}" + +XORG_BIN="${DASHBOARD_XORG_BIN:-}" +if [[ -z "$XORG_BIN" ]]; then + if command -v Xorg >/dev/null 2>&1; then + XORG_BIN="$(command -v Xorg)" + elif [[ -x /usr/lib/xorg/Xorg ]]; then + XORG_BIN=/usr/lib/xorg/Xorg + elif [[ -x /usr/bin/Xorg ]]; then + XORG_BIN=/usr/bin/Xorg + else + echo "dashboard.sh: Xorg not found in PATH and common locations" >&2 + exit 127 + fi +fi + # Create a temp conky config file CFG="$(mktemp /tmp/conky-dashboard.XXXXXX.conf)" cleanup() { rm -f "$CFG"; } @@ -37,7 +54,7 @@ ${color 666666}Procs:${color} $processes ${color 666666}Running:${color} $runn EOF # Start X just for this dashboard. Conky runs in the foreground; Ctrl-C exits X. -exec xinit /usr/bin/env bash -lc ' +exec /usr/bin/xinit /usr/bin/env bash -lc ' # Disable screen blanking + power management xset s off xset -dpms @@ -45,4 +62,4 @@ exec xinit /usr/bin/env bash -lc ' xsetroot -solid "#202020" exec conky -c "'"$CFG"'" -' -- :0 + ' -- "$XORG_BIN" "$DASHBOARD_DISPLAY" "vt${DASHBOARD_VT}" -keeptty -nolisten tcp