diff --git a/scripts/deploy-plan.py b/scripts/deploy-plan.py index 663db2f..c1dc1f5 100755 --- a/scripts/deploy-plan.py +++ b/scripts/deploy-plan.py @@ -50,6 +50,8 @@ SERVICE_ORDER = ( "copyparty", "jellyfin", "calibre-web", + "plex", + "tautulli", "prowlarr", "qbittorrent", "radarr", @@ -99,6 +101,8 @@ DANKSERVER_SERVICES = { "copyparty", "jellyfin", "calibre-web", + "plex", + "tautulli", "prowlarr", "qbittorrent", "radarr", @@ -329,6 +333,8 @@ def services_for_path(path: str, target: str) -> set[str]: "jellyfin": {"jellyfin"}, "calibre-web": {"calibre-web"}, "copyparty": {"copyparty"}, + "plex": {"plex"}, + "tautulli": {"tautulli"}, } _add(services, *quadlet_services.get(parts[1], set())) return services diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh index 88f6bf8..3793de2 100755 --- a/scripts/deploy-remote.sh +++ b/scripts/deploy-remote.sh @@ -93,7 +93,7 @@ service_is_allowed() { dankclaw:frank-default-large|dankclaw:frank-default-npu|\ dankclaw:frank-default-sightsound|\ dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\ - dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|\ + dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:plex|dankserver:tautulli|\ dankserver:prowlarr|dankserver:qbittorrent|dankserver:radarr|\ dankserver:sonarr|dankserver:lidarr|dankserver:readarr|\ dankserver:frank-default-small|\ @@ -666,6 +666,43 @@ deploy_copyparty() { systemctl --user restart copyparty.service } +deploy_plex() { + cp "dankserver/plex/quadlet/plex.container" "$quadlet_unit_dir/" + systemctl --user daemon-reload + if ! systemctl --user is-active -q plex.service; then + # First cutover: retire the native package unit BEFORE starting the + # container — both cannot own 32400. Mask (not disable): on these hosts + # `systemctl disable` deletes the unit file itself; mask keeps an + # instant byte-safe rollback while nothing is signed off yet. + run_privileged systemctl stop plexmediaserver.service + run_privileged systemctl mask plexmediaserver.service + fi + systemctl --user restart plex.service + # Readiness gate: /identity answers MediaContainer XML once PMS is up; + # fail the deploy (pending state preserved) rather than mark it deployed blind. + for ((i = 1; i <= 36; i++)); do + curl -fsS --max-time 3 http://127.0.0.1:32400/identity >/dev/null && return 0 + sleep 5 + done + echo "FAIL: plex did not answer /identity within 180s" >&2 + return 1 +} + +deploy_tautulli() { + cp "dankserver/tautulli/quadlet/tautulli.container" "$quadlet_unit_dir/" + systemctl --user daemon-reload + if ! systemctl --user is-active -q tautulli.service; then + run_privileged snap stop tautulli || true + run_privileged snap disable tautulli || true + mkdir -p /home/dank/Containers/tautulli/config + if [[ ! -f /home/dank/Containers/tautulli/config/config.ini ]]; then + printf '[General]\nPMS_Web_Host = plex\nPMS_Web_Port = 32400\nPMS_Secure = 0\n' \ + > /home/dank/Containers/tautulli/config/config.ini + fi + fi + systemctl --user restart tautulli.service +} + deploy_authentik() { local unit mkdir -p "$quadlet_unit_dir" @@ -1009,6 +1046,8 @@ deploy_service() { jellyfin) deploy_jellyfin ;; calibre-web) deploy_calibre_web ;; copyparty) deploy_copyparty ;; + plex) deploy_plex ;; + tautulli) deploy_tautulli ;; *) deploy_compose_service "$service" ;; esac } diff --git a/scripts/deploy.sh b/scripts/deploy.sh index da06a8e..5d2d272 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -137,7 +137,7 @@ valid_service() { dankclaw:frank-default-large|dankclaw:frank-default-npu|\ dankclaw:frank-default-sightsound|\ dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\ - dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:prowlarr|\ + dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:plex|dankserver:tautulli|\ dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\ dankserver:lidarr|dankserver:readarr|dankserver:node-exporter|\ dankserver:frank-default-small|\ diff --git a/scripts/test-changed.sh b/scripts/test-changed.sh index 0186aa4..dd7497a 100755 --- a/scripts/test-changed.sh +++ b/scripts/test-changed.sh @@ -209,9 +209,9 @@ for path in "${CHANGED[@]}"; do add_test tests/scripts_syntax.bats ;; # Cutover-era dankserver quadlet units (romm trio, jellyfin, calibre-web, - # copyparty): the seam checks boot-persistence + v1-generator contract; - # deploy-flow seams guard the deployer routing that installs them. - dankserver/romm/*|dankserver/jellyfin/*|dankserver/calibre-web/*|dankserver/copyparty/*) + # copyparty, plex, tautulli): the seam checks boot-persistence + + # v1-generator contract; deploy-flow seams guard the deployer routing. + dankserver/romm/*|dankserver/jellyfin/*|dankserver/calibre-web/*|dankserver/copyparty/*|dankserver/plex/*|dankserver/tautulli/*) add_test tests/dankserver_quadlet.bats add_test tests/deployment_flow.bats add_test tests/deployment_scoping.bats diff --git a/tests/dankserver_quadlet.bats b/tests/dankserver_quadlet.bats index 952acbd..e4dc226 100755 --- a/tests/dankserver_quadlet.bats +++ b/tests/dankserver_quadlet.bats @@ -1,20 +1,20 @@ #!/usr/bin/env bats -# Seam: the six cutover-era dankserver quadlet units (romm trio, jellyfin, -# calibre-web, copyparty) must keep their boot-persistence and v1-generator +# Seam: the eight cutover-era dankserver quadlet units (romm trio, jellyfin, +# calibre-web, copyparty, plex, tautulli) must keep their boot-persistence and v1-generator # contracts. The podman 5.x v1 Quadlet generator silently drops or hard-fails # on several keys; these tests pin the contract that keeps the units loadable # and secret-free in the repo. load helpers -@test "all six cutover units exist under dankserver//quadlet/" { - for unit in romm/quadlet/romm romm/quadlet/romm-mariadb romm/quadlet/romm-redis jellyfin/quadlet/jellyfin calibre-web/quadlet/calibre-web copyparty/quadlet/copyparty; do +@test "all eight cutover units exist under dankserver//quadlet/" { + for unit in romm/quadlet/romm romm/quadlet/romm-mariadb romm/quadlet/romm-redis jellyfin/quadlet/jellyfin calibre-web/quadlet/calibre-web copyparty/quadlet/copyparty plex/quadlet/plex tautulli/quadlet/tautulli; do [ -f "$REPO_ROOT/dankserver/$unit.container" ] || return 1 done } @test "every cutover unit is boot-persistent (WantedBy=default.target)" { - for unit in romm/quadlet/romm romm/quadlet/romm-mariadb romm/quadlet/romm-redis jellyfin/quadlet/jellyfin calibre-web/quadlet/calibre-web copyparty/quadlet/copyparty; do + for unit in romm/quadlet/romm romm/quadlet/romm-mariadb romm/quadlet/romm-redis jellyfin/quadlet/jellyfin calibre-web/quadlet/calibre-web copyparty/quadlet/copyparty plex/quadlet/plex tautulli/quadlet/tautulli; do grep -q '^WantedBy=default.target$' "$REPO_ROOT/dankserver/$unit.container" || return 1 done } @@ -51,7 +51,8 @@ load helpers # above. Keep this list current with every generator finding. for unit in dankserver/romm/quadlet/romm dankserver/romm/quadlet/romm-mariadb \ dankserver/romm/quadlet/romm-redis dankserver/jellyfin/quadlet/jellyfin \ - dankserver/calibre-web/quadlet/calibre-web dankserver/copyparty/quadlet/copyparty; do + dankserver/calibre-web/quadlet/calibre-web dankserver/copyparty/quadlet/copyparty \ + dankserver/plex/quadlet/plex dankserver/tautulli/quadlet/tautulli; do ! grep -q '^Device=' "$REPO_ROOT/$unit.container" || return 1 done grep -q '^AddDevice=/dev/dri$' "$REPO_ROOT/dankserver/jellyfin/quadlet/jellyfin.container" || return 1 @@ -62,4 +63,18 @@ load helpers grep -q '^PublishPort=8096:8096$' "$REPO_ROOT/dankserver/jellyfin/quadlet/jellyfin.container" || return 1 grep -q '^PublishPort=8083:8083$' "$REPO_ROOT/dankserver/calibre-web/quadlet/calibre-web.container" || return 1 grep -q '^PublishPort=3923:3923$' "$REPO_ROOT/dankserver/copyparty/quadlet/copyparty.container" || return 1 + grep -q '^PublishPort=32400:32400$' "$REPO_ROOT/dankserver/plex/quadlet/plex.container" || return 1 + grep -q '^PublishPort=8181:8181$' "$REPO_ROOT/dankserver/tautulli/quadlet/tautulli.container" || return 1 +} + +@test "plex keeps the native unit's identity (userns=host, PUID=997, PGID=984)" { + # Zero-chown ownership contract: /var/lib/plexmediaserver is bind-mounted + # in place, so every file the container writes must land owned 997:984 — + # byte-identical to the former plexmediaserver.service identity. If the + # v1 generator ever drops UsernsMode=, files appear as the rootless + # subuid and the migration invariant is broken; pin both keys. + unit="$REPO_ROOT/dankserver/plex/quadlet/plex.container" + grep -q '^UsernsMode=host$' "$unit" || return 1 + grep -q '^Environment=PUID=997$' "$unit" || return 1 + grep -q '^Environment=PGID=984$' "$unit" || return 1 }