frankbook-pro: *arr stack + qbittorrent quadlets inside podman machine
Seven units installed INSIDE the Mac VM (/home/core/.config/containers/ systemd) by a new deploy_frankbook_stack handler: gluetun (ProtonVPN WireGuard, env-file credentials, publishes qbit's 8080/6881), qbittorrent (Network=gluetun.container, verbatim /Volumes data roots, first-boot config seed with fresh machine-local WebUI password and the Finished Torrents category), and prowlarr/sonarr/radarr/lidarr/readarr (grab-only, config migrated from dankserver at provisioning, readarr pinned to amd64-1.0.980). deploy.sh routes the new target through the ssh-config alias with REMOTE_SHELL=/usr/local/bin/bash (macOS bash 3.2 lacks associative arrays); the planner learns FRANKBOOK_SERVICES and the gluetun->qbittorrent netns pairing; new seam test pins the contract.
This commit is contained in:
1 parent
4d5bea42e0
commit
949f68e2b5
13 files changed
+516
-14
No files matched your search
@@ -0,0 +1,43 @@
|
|||||||
|
# Quadlet unit: Gluetun VPN tunnel in FrankBook-Pro's podman machine.
|
||||||
|
#
|
||||||
|
# Install path is INSIDE the machine VM:
|
||||||
|
# /home/core/.config/containers/systemd/gluetun.container
|
||||||
|
# (installed by the frankbook-pro handler in scripts/deploy-remote.sh —
|
||||||
|
# install_quadlet targets the Linux hosts' systemctl --user, not the VM).
|
||||||
|
#
|
||||||
|
# WireGuard tunnel to ProtonVPN. qBittorrent shares this container's network
|
||||||
|
# namespace (Network=gluetun.container) so torrent egress never touches the
|
||||||
|
# residential IP. qBittorrent's WebUI and BitTorrent ports publish HERE — a
|
||||||
|
# netns-sharer publishes no ports of its own (same invariant as the dankclaw
|
||||||
|
# searxng/gluetun pair). Credentials live in
|
||||||
|
# /home/core/Containers/gluetun/env inside the VM (mode 0600, never
|
||||||
|
# committed); the private key is Mac-tunnel-specific and MUST NOT be copied
|
||||||
|
# from dankclaw — one WireGuard peer key cannot serve two machines at once.
|
||||||
|
[Unit]
|
||||||
|
Description=Gluetun VPN tunnel (FrankBook-Pro podman machine)
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=gluetun
|
||||||
|
Image=docker.io/qmcgaw/gluetun:latest
|
||||||
|
AddCapability=NET_ADMIN
|
||||||
|
AddDevice=/dev/net/tun:/dev/net/tun
|
||||||
|
Environment=VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER}
|
||||||
|
Environment=VPN_TYPE=${VPN_TYPE}
|
||||||
|
Environment=WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
|
||||||
|
Environment=WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
|
||||||
|
Environment=WIREGUARD_PUBLIC_KEY=${WIREGUARD_PUBLIC_KEY}
|
||||||
|
Environment=WIREGUARD_ENDPOINT_IP=${WIREGUARD_ENDPOINT_IP}
|
||||||
|
Environment=WIREGUARD_ENDPOINT_PORT=${WIREGUARD_ENDPOINT_PORT}
|
||||||
|
Environment=WIREGUARD_ALLOWED_IPS=${WIREGUARD_ALLOWED_IPS}
|
||||||
|
Environment=WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL=${WIREGUARD_PERSISTENT_KEEPALIVE_INTERVAL}
|
||||||
|
PublishPort=8080:8080
|
||||||
|
PublishPort=6881:6881
|
||||||
|
PublishPort=6881:6881/udp
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
EnvironmentFile=/home/core/Containers/gluetun/env
|
||||||
|
TimeoutStartSec=900
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Quadlet unit: Lidarr in FrankBook-Pro's podman machine.
|
||||||
|
#
|
||||||
|
# Install path is INSIDE the machine VM:
|
||||||
|
# /home/core/.config/containers/systemd/lidarr.container
|
||||||
|
#
|
||||||
|
# Grab-only: no media binds; config migrated from dankserver's retired
|
||||||
|
# bare container.
|
||||||
|
[Unit]
|
||||||
|
Description=Lidarr (FrankBook-Pro)
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=lidarr
|
||||||
|
Image=lscr.io/linuxserver/lidarr:latest
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/Los_Angeles
|
||||||
|
Environment=UMASK_SET=022
|
||||||
|
Volume=/Volumes/COMPLETE/lidarr-data:/config
|
||||||
|
PublishPort=8686:8686
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
TimeoutStartSec=900
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Quadlet unit: Prowlarr in FrankBook-Pro's podman machine.
|
||||||
|
#
|
||||||
|
# Install path is INSIDE the machine VM:
|
||||||
|
# /home/core/.config/containers/systemd/prowlarr.container
|
||||||
|
#
|
||||||
|
# Arr on the Mac is grab-only: indexer + app-sync management lives here, the
|
||||||
|
# arrs never auto-import media (the user reviews completed torrents in
|
||||||
|
# /Volumes/COMPLETE/"Finished Torrents" and moves files manually into the
|
||||||
|
# library, which lives on dankserver's entropy disk).
|
||||||
|
#
|
||||||
|
# Config is migrated from dankserver's retired bare container
|
||||||
|
# (tar-archived first, per the Phase 4 cutover note) — indexers and app
|
||||||
|
# settings survive; stale host pointers (old qbit at 192.168.0.235:8080)
|
||||||
|
# are re-pointed to the Mac's qbit via its API after first boot.
|
||||||
|
[Unit]
|
||||||
|
Description=Prowlarr (FrankBook-Pro)
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=prowlarr
|
||||||
|
Image=lscr.io/linuxserver/prowlarr:latest
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/Los_Angeles
|
||||||
|
Environment=UMASK_SET=022
|
||||||
|
Volume=/Volumes/COMPLETE/prowlarr-data:/config
|
||||||
|
PublishPort=9696:9696
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
TimeoutStartSec=900
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Quadlet unit: qBittorrent in FrankBook-Pro's podman machine.
|
||||||
|
#
|
||||||
|
# Install path is INSIDE the machine VM:
|
||||||
|
# /home/core/.config/containers/systemd/qbittorrent.container
|
||||||
|
#
|
||||||
|
# Joins gluetun's network namespace (Network=gluetun.container) so all
|
||||||
|
# BitTorrent egress exits the WireGuard tunnel. It publishes NO port of its
|
||||||
|
# own — WebUI 8080 and torrent port 6881 publish on the gluetun unit.
|
||||||
|
#
|
||||||
|
# Data layout (plan-locked, verbatim paths):
|
||||||
|
# /config <- /Volumes/COMPLETE/qbittorrent-data (durable config)
|
||||||
|
# /Volumes/COMPLETE + /Volumes/INCOMPLETE same-path virtiofs passthroughs —
|
||||||
|
# the seeded qBittorrent.conf references those literal paths:
|
||||||
|
# Session\DefaultSavePath=/Volumes/COMPLETE
|
||||||
|
# Session\TempPath=/Volumes/INCOMPLETE/incomplete (in-progress disk)
|
||||||
|
# category "Finished Torrents" -> /Volumes/COMPLETE/Finished Torrents
|
||||||
|
# (the human review dir; nothing automated ever writes to entropy).
|
||||||
|
#
|
||||||
|
# WebUI password: generated fresh at provisioning by the deploy handler,
|
||||||
|
# stored machine-local only (conf + /home/core/Containers/qbittorrent/env).
|
||||||
|
[Unit]
|
||||||
|
Description=qBittorrent behind gluetun (FrankBook-Pro)
|
||||||
|
Wants=gluetun.service
|
||||||
|
After=gluetun.service
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=qbittorrent
|
||||||
|
Network=gluetun.container
|
||||||
|
Image=lscr.io/linuxserver/qbittorrent:latest
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/Los_Angeles
|
||||||
|
Environment=UMASK_SET=022
|
||||||
|
Volume=/Volumes/COMPLETE/qbittorrent-data:/config
|
||||||
|
Volume=/Volumes/COMPLETE:/Volumes/COMPLETE
|
||||||
|
Volume=/Volumes/INCOMPLETE:/Volumes/INCOMPLETE
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
TimeoutStartSec=900
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Quadlet unit: Radarr in FrankBook-Pro's podman machine.
|
||||||
|
#
|
||||||
|
# Install path is INSIDE the machine VM:
|
||||||
|
# /home/core/.config/containers/systemd/radarr.container
|
||||||
|
#
|
||||||
|
# Grab-only: no media binds; config migrated from dankserver's retired
|
||||||
|
# bare container.
|
||||||
|
[Unit]
|
||||||
|
Description=Radarr (FrankBook-Pro)
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=radarr
|
||||||
|
Image=lscr.io/linuxserver/radarr:latest
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/Los_Angeles
|
||||||
|
Environment=UMASK_SET=022
|
||||||
|
Volume=/Volumes/COMPLETE/radarr-data:/config
|
||||||
|
PublishPort=7878:7878
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
TimeoutStartSec=900
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Quadlet unit: Readarr in FrankBook-Pro's podman machine.
|
||||||
|
#
|
||||||
|
# Install path is INSIDE the machine VM:
|
||||||
|
# /home/core/.config/containers/systemd/readarr.container
|
||||||
|
#
|
||||||
|
# Grab-only: no media binds; config migrated from dankserver's retired
|
||||||
|
# bare container.
|
||||||
|
#
|
||||||
|
# Image pinned to amd64-1.0.980 (per project image-pin policy — same tag the
|
||||||
|
# dankserver compose ran): `latest` historically shipped an amd64-only build
|
||||||
|
# of a broken 1.x nightly for Readarr; the pin avoids surprises.
|
||||||
|
[Unit]
|
||||||
|
Description=Readarr (FrankBook-Pro)
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=readarr
|
||||||
|
Image=lscr.io/linuxserver/readarr:amd64-1.0.980
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/Los_Angeles
|
||||||
|
Environment=UMASK_SET=022
|
||||||
|
Volume=/Volumes/COMPLETE/readarr-data:/config
|
||||||
|
PublishPort=8787:8787
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
TimeoutStartSec=900
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Quadlet unit: Sonarr in FrankBook-Pro's podman machine.
|
||||||
|
#
|
||||||
|
# Install path is INSIDE the machine VM:
|
||||||
|
# /home/core/.config/containers/systemd/sonarr.container
|
||||||
|
#
|
||||||
|
# Grab-only: no media binds (the TV library lives on dankserver's entropy
|
||||||
|
# disk, which the Mac never mounts); config migrated from dankserver's
|
||||||
|
# retired bare container.
|
||||||
|
[Unit]
|
||||||
|
Description=Sonarr (FrankBook-Pro)
|
||||||
|
|
||||||
|
[Container]
|
||||||
|
ContainerName=sonarr
|
||||||
|
Image=lscr.io/linuxserver/sonarr:latest
|
||||||
|
Environment=PUID=0
|
||||||
|
Environment=PGID=0
|
||||||
|
Environment=TZ=America/Los_Angeles
|
||||||
|
Environment=UMASK_SET=022
|
||||||
|
Volume=/Volumes/COMPLETE/sonarr-data:/config
|
||||||
|
PublishPort=8989:8989
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
TimeoutStartSec=900
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
+28
-1
@@ -104,6 +104,20 @@ DANKSERVER_SERVICES = {
|
|||||||
"frank-default-small",
|
"frank-default-small",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# FrankBook-Pro runs the *arr stack + qBittorrent inside a podman machine on
|
||||||
|
# macOS. Service names overlap the Linux hosts (gluetun, qbittorrent, ...);
|
||||||
|
# the per-target sets and the frankbook-pro intercept in deploy-remote.sh
|
||||||
|
# keep dispatch unambiguous.
|
||||||
|
FRANKBOOK_SERVICES = {
|
||||||
|
"gluetun",
|
||||||
|
"qbittorrent",
|
||||||
|
"prowlarr",
|
||||||
|
"radarr",
|
||||||
|
"sonarr",
|
||||||
|
"lidarr",
|
||||||
|
"readarr",
|
||||||
|
}
|
||||||
|
|
||||||
DANKCLAW_COMPOSE_SERVICES = {
|
DANKCLAW_COMPOSE_SERVICES = {
|
||||||
"comfyui",
|
"comfyui",
|
||||||
"hermes",
|
"hermes",
|
||||||
@@ -147,6 +161,8 @@ def target_services(target: str) -> set[str]:
|
|||||||
return DANKCLAW_SERVICES
|
return DANKCLAW_SERVICES
|
||||||
if target == "dankserver":
|
if target == "dankserver":
|
||||||
return DANKSERVER_SERVICES
|
return DANKSERVER_SERVICES
|
||||||
|
if target == "frankbook-pro":
|
||||||
|
return FRANKBOOK_SERVICES
|
||||||
raise ValueError(f"unknown target: {target}")
|
raise ValueError(f"unknown target: {target}")
|
||||||
|
|
||||||
|
|
||||||
@@ -313,6 +329,17 @@ def services_for_path(path: str, target: str) -> set[str]:
|
|||||||
_add(services, *quadlet_services.get(parts[1], set()))
|
_add(services, *quadlet_services.get(parts[1], set()))
|
||||||
return services
|
return services
|
||||||
|
|
||||||
|
if parts[0] == "frankbook-pro":
|
||||||
|
if len(parts) >= 3 and parts[2] == "quadlet":
|
||||||
|
name = parts[3].removesuffix(".container")
|
||||||
|
if name == "gluetun":
|
||||||
|
# qBittorrent shares gluetun's network namespace; a gluetun
|
||||||
|
# recreation requires both units to be restarted.
|
||||||
|
_add(services, "gluetun", "qbittorrent")
|
||||||
|
elif name in FRANKBOOK_SERVICES:
|
||||||
|
_add(services, name)
|
||||||
|
return services
|
||||||
|
|
||||||
if parts[0] == "scripts":
|
if parts[0] == "scripts":
|
||||||
name = p.name
|
name = p.name
|
||||||
if name in {"frank-default-large.env", "frank-default-large.service", "serve-frank-large.sh"}:
|
if name in {"frank-default-large.env", "frank-default-large.service", "serve-frank-large.sh"}:
|
||||||
@@ -362,7 +389,7 @@ def plan(paths: list[str], target: str, repo: str | None = None,
|
|||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument("--target", choices=("dankclaw", "dankserver"), required=True)
|
parser.add_argument("--target", choices=("dankclaw", "dankserver", "frankbook-pro"), required=True)
|
||||||
parser.add_argument("--paths", nargs="+")
|
parser.add_argument("--paths", nargs="+")
|
||||||
parser.add_argument("--repo", help="repository path for revision-aware planning")
|
parser.add_argument("--repo", help="repository path for revision-aware planning")
|
||||||
parser.add_argument("--base", help="base Git revision")
|
parser.add_argument("--base", help="base Git revision")
|
||||||
|
|||||||
+131
-3
@@ -18,7 +18,7 @@ SERVICES=()
|
|||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
Usage: deploy-remote.sh <dankclaw|dankserver> <git-revision> [--pending] [service]...
|
Usage: deploy-remote.sh <dankclaw|dankserver|frankbook-pro> <git-revision> [--pending] [service]...
|
||||||
|
|
||||||
The service list is explicit. An empty list is a successful no-op.
|
The service list is explicit. An empty list is a successful no-op.
|
||||||
EOF
|
EOF
|
||||||
@@ -57,7 +57,7 @@ run_privileged() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ "$TARGET" != dankclaw && "$TARGET" != dankserver ]]; then
|
if [[ "$TARGET" != dankclaw && "$TARGET" != dankserver && "$TARGET" != frankbook-pro ]]; then
|
||||||
usage >&2
|
usage >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
@@ -96,7 +96,10 @@ service_is_allowed() {
|
|||||||
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|\
|
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|\
|
||||||
dankserver:prowlarr|dankserver:qbittorrent|dankserver:radarr|\
|
dankserver:prowlarr|dankserver:qbittorrent|dankserver:radarr|\
|
||||||
dankserver:sonarr|dankserver:lidarr|dankserver:readarr|\
|
dankserver:sonarr|dankserver:lidarr|dankserver:readarr|\
|
||||||
dankserver:frank-default-small)
|
dankserver:frank-default-small|\
|
||||||
|
frankbook-pro:gluetun|frankbook-pro:qbittorrent|frankbook-pro:prowlarr|\
|
||||||
|
frankbook-pro:radarr|frankbook-pro:sonarr|frankbook-pro:lidarr|\
|
||||||
|
frankbook-pro:readarr)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -814,8 +817,133 @@ wait_for_health() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# FrankBook-Pro (macOS): quadlet units live INSIDE the podman machine VM
|
||||||
|
# (/home/core/.config/containers/systemd), not in the macOS user session.
|
||||||
|
# This handler pipes unit files and seed logic into the VM over
|
||||||
|
# `podman machine ssh`; it never touches systemctl on the Mac host and never
|
||||||
|
# uses run_privileged (the Mac account is not passwordless sudo). Unit files
|
||||||
|
# are re-piped on every call (idempotent, cheap) before the selected unit is
|
||||||
|
# restarted; first boot also seeds the qBittorrent config once.
|
||||||
|
FRANKBOOK_SVCS=(gluetun qbittorrent prowlarr sonarr radarr lidarr readarr)
|
||||||
|
deploy_frankbook_stack() {
|
||||||
|
local service="$1"
|
||||||
|
export PATH="/usr/local/bin:/opt/homebrew/bin:$PATH"
|
||||||
|
if ! podman machine inspect --format '{{.State}}' 2>/dev/null | grep -qx running; then
|
||||||
|
echo "FAIL: podman machine is not running on frankbook-pro" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
local vm_dir="/home/core/.config/containers/systemd" svc
|
||||||
|
podman machine ssh "mkdir -p '$vm_dir' /home/core/Containers/gluetun /home/core/Containers/qbittorrent"
|
||||||
|
for svc in "${FRANKBOOK_SVCS[@]}"; do
|
||||||
|
podman machine ssh "cat > '$vm_dir/$svc.container'" \
|
||||||
|
< "frankbook-pro/$svc/quadlet/$svc.container"
|
||||||
|
done
|
||||||
|
local tmp
|
||||||
|
tmp="$(mktemp /tmp/frankbook-vm.XXXXXX)"
|
||||||
|
cat > "$tmp" <<'VM_SETUP'
|
||||||
|
set -euo pipefail
|
||||||
|
SVC="${1:?service required}"
|
||||||
|
# qBittorrent first-boot seed: conf + categories + machine-local password.
|
||||||
|
if [ ! -e /home/core/Containers/qbittorrent/qbit-seeded ]; then
|
||||||
|
mkdir -p /Volumes/COMPLETE/qbittorrent-data/qBittorrent
|
||||||
|
QBIT_PW="$(openssl rand -base64 18 | tr -d '/+=')"
|
||||||
|
umask 077
|
||||||
|
printf 'QBIT_WEBUI_PASSWORD=%s\n' "$QBIT_PW" > /home/core/Containers/qbittorrent/env
|
||||||
|
umask 022
|
||||||
|
{
|
||||||
|
cat <<'QCONF'
|
||||||
|
[AutoRun]
|
||||||
|
|
||||||
|
[BitTorrent]
|
||||||
|
Session\DefaultSavePath=/Volumes/COMPLETE
|
||||||
|
Session\Port=6881
|
||||||
|
Session\QueueingSystemEnabled=true
|
||||||
|
Session\TempPath=/Volumes/INCOMPLETE/incomplete
|
||||||
|
Session\TempPathEnabled=true
|
||||||
|
|
||||||
|
[LegalNotice]
|
||||||
|
|
||||||
|
[Meta]
|
||||||
|
MigrationVersion=8
|
||||||
|
|
||||||
|
[Network]
|
||||||
|
Cookies\Enabled=true
|
||||||
|
|
||||||
|
[Preferences]
|
||||||
|
WebUI\Address=*
|
||||||
|
WebUI\Username=admin
|
||||||
|
QCONF
|
||||||
|
printf 'WebUI\\Password=%s\n' "$QBIT_PW"
|
||||||
|
} > /Volumes/COMPLETE/qbittorrent-data/qBittorrent/qBittorrent.conf
|
||||||
|
cat > /Volumes/COMPLETE/qbittorrent-data/qBittorrent/categories.json <<'QCAT'
|
||||||
|
{
|
||||||
|
"Finished Torrents": {
|
||||||
|
"save_path": "/Volumes/COMPLETE/Finished Torrents"
|
||||||
|
},
|
||||||
|
"lidarr": {"save_path": ""},
|
||||||
|
"radarr": {"save_path": ""},
|
||||||
|
"tv-sonarr": {"save_path": ""}
|
||||||
|
}
|
||||||
|
QCAT
|
||||||
|
touch /home/core/Containers/qbittorrent/qbit-seeded
|
||||||
|
echo " seeded qBittorrent config (WebUI password: /home/core/Containers/qbittorrent/env inside the VM)"
|
||||||
|
fi
|
||||||
|
if [ "$SVC" = gluetun ] && [ ! -s /home/core/Containers/gluetun/env ]; then
|
||||||
|
echo "SKIP: gluetun has no credentials yet."
|
||||||
|
echo " Write the ProtonVPN WireGuard keys into /home/core/Containers/gluetun/env inside the VM"
|
||||||
|
echo " (podman machine ssh), then re-run: ./scripts/deploy.sh frankbook-pro --service gluetun"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ "$SVC" = qbittorrent ] && [ ! -s /home/core/Containers/gluetun/env ]; then
|
||||||
|
echo "SKIP: qbittorrent waits for gluetun credentials."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
unit="$(systemctl --user list-units --all --no-legend "${SVC}.service" "container-${SVC}.service" 2>/dev/null | head -n1 | awk '{print $1}')"
|
||||||
|
[ -n "$unit" ] || { echo "FAIL: no generated systemd unit for $SVC" >&2; exit 1; }
|
||||||
|
systemctl --user restart "$unit"
|
||||||
|
sleep 3
|
||||||
|
state="$(systemctl --user is-active "$unit" || true)"
|
||||||
|
if [ "$state" != active ] && [ "$state" != activating ]; then
|
||||||
|
echo "FAIL: $unit is $state" >&2
|
||||||
|
systemctl --user status "$unit" --no-pager -l || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
VM_SETUP
|
||||||
|
podman machine ssh bash -s "$service" < "$tmp"
|
||||||
|
rm -f "$tmp"
|
||||||
|
local port path
|
||||||
|
case "$service" in
|
||||||
|
qbittorrent) port=8080; path=/ ;;
|
||||||
|
prowlarr) port=9696; path=/ping ;;
|
||||||
|
sonarr) port=8989; path=/ping ;;
|
||||||
|
radarr) port=7878; path=/ping ;;
|
||||||
|
lidarr) port=8686; path=/ping ;;
|
||||||
|
readarr) port=8787; path=/ping ;;
|
||||||
|
*) return 0 ;; # gluetun publishes only qBittorrent's ports
|
||||||
|
esac
|
||||||
|
printf ' waiting for http://127.0.0.1:%s%s on the Mac...\n' "$port" "$path"
|
||||||
|
local ok=0 i
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
if curl -fsS -m 4 "http://127.0.0.1:${port}${path}" >/dev/null 2>&1; then
|
||||||
|
ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
[[ "$ok" -eq 1 ]] || { echo "FAIL: $service did not answer on 127.0.0.1:$port" >&2; return 1; }
|
||||||
|
echo " $service answers on 127.0.0.1:$port"
|
||||||
|
}
|
||||||
|
|
||||||
deploy_service() {
|
deploy_service() {
|
||||||
local service="$1"
|
local service="$1"
|
||||||
|
if [[ "$TARGET" == "frankbook-pro" ]]; then
|
||||||
|
# All frankbook-pro services route through the Mac handler: the name
|
||||||
|
# case below addresses the Linux hosts, and service names overlap.
|
||||||
|
deploy_frankbook_stack "$service"
|
||||||
|
return
|
||||||
|
fi
|
||||||
case "$service" in
|
case "$service" in
|
||||||
caddy) deploy_caddy ;;
|
caddy) deploy_caddy ;;
|
||||||
homepage) deploy_homepage ;;
|
homepage) deploy_homepage ;;
|
||||||
|
|||||||
+30
-10
@@ -7,7 +7,7 @@
|
|||||||
# restarted only when its own files (or the shared chat template) are selected.
|
# restarted only when its own files (or the shared chat template) are selected.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/deploy.sh <dankclaw|dankserver> [--service NAME]... [--dry-run]
|
# ./scripts/deploy.sh <dankclaw|dankserver|frankbook-pro> [--service NAME]... [--dry-run]
|
||||||
#
|
#
|
||||||
# Without --service, the planner selects services changed since origin/main and
|
# Without --service, the planner selects services changed since origin/main and
|
||||||
# the remote runner also applies pending service changes from its state store.
|
# the remote runner also applies pending service changes from its state store.
|
||||||
@@ -20,10 +20,11 @@ REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
|||||||
PLANNER="$SCRIPT_DIR/deploy-plan.py"
|
PLANNER="$SCRIPT_DIR/deploy-plan.py"
|
||||||
REMOTE_USER="dank"
|
REMOTE_USER="dank"
|
||||||
REMOTE_DIR="/home/dank/serverAppConfigs"
|
REMOTE_DIR="/home/dank/serverAppConfigs"
|
||||||
|
REMOTE_SHELL="bash"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<'EOF'
|
cat <<'EOF'
|
||||||
Usage: deploy.sh <dankclaw|dankserver> [options]
|
Usage: deploy.sh <dankclaw|dankserver|frankbook-pro> [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--service NAME deploy only NAME (repeatable; bypasses automatic planning)
|
--service NAME deploy only NAME (repeatable; bypasses automatic planning)
|
||||||
@@ -50,8 +51,21 @@ fi
|
|||||||
shift
|
shift
|
||||||
|
|
||||||
case "$TARGET" in
|
case "$TARGET" in
|
||||||
dankclaw) REMOTE_HOST="dankclaw" ;;
|
dankclaw)
|
||||||
dankserver) REMOTE_HOST="dankserver" ;;
|
REMOTE_HOST="dankclaw"
|
||||||
|
;;
|
||||||
|
dankserver)
|
||||||
|
REMOTE_HOST="dankserver"
|
||||||
|
;;
|
||||||
|
frankbook-pro)
|
||||||
|
REMOTE_HOST="frankbook-pro"
|
||||||
|
REMOTE_USER="dank"
|
||||||
|
REMOTE_DIR="/Users/dank/Projects/serverAppConfigs"
|
||||||
|
# macOS ships bash 3.2; the runner needs bash >= 4 (associative
|
||||||
|
# arrays). /usr/local/bin/bash is the Intel-homebrew interpreter,
|
||||||
|
# provisioned with the Mac checkout.
|
||||||
|
REMOTE_SHELL="/usr/local/bin/bash"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown target: $TARGET" >&2
|
echo "Unknown target: $TARGET" >&2
|
||||||
usage >&2
|
usage >&2
|
||||||
@@ -126,7 +140,10 @@ valid_service() {
|
|||||||
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:prowlarr|\
|
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:prowlarr|\
|
||||||
dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\
|
dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\
|
||||||
dankserver:lidarr|dankserver:readarr|dankserver:node-exporter|\
|
dankserver:lidarr|dankserver:readarr|dankserver:node-exporter|\
|
||||||
dankserver:frank-default-small)
|
dankserver:frank-default-small|\
|
||||||
|
frankbook-pro:gluetun|frankbook-pro:qbittorrent|frankbook-pro:prowlarr|\
|
||||||
|
frankbook-pro:radarr|frankbook-pro:sonarr|frankbook-pro:lidarr|\
|
||||||
|
frankbook-pro:readarr)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -174,7 +191,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
REMOTE="${REMOTE_USER}@${REMOTE_HOST}"
|
REMOTE="${REMOTE_USER}@${REMOTE_HOST}"
|
||||||
REMOTE_ARGS=("$TARGET" "$HEAD_REVISION")
|
REMOTE_ARGS=("$TARGET" "$HEAD_REVISION" "$REMOTE_DIR" "$REMOTE_SHELL")
|
||||||
if [[ "${#EXPLICIT_SERVICES[@]}" -eq 0 ]]; then
|
if [[ "${#EXPLICIT_SERVICES[@]}" -eq 0 ]]; then
|
||||||
REMOTE_ARGS+=(--pending)
|
REMOTE_ARGS+=(--pending)
|
||||||
fi
|
fi
|
||||||
@@ -196,12 +213,15 @@ echo "[2/2] Pulling and deploying on $REMOTE_HOST..."
|
|||||||
# The target runner computes any pending revisions from its own state after
|
# The target runner computes any pending revisions from its own state after
|
||||||
# the pull. This keeps a target that missed an earlier service deployment
|
# the pull. This keeps a target that missed an earlier service deployment
|
||||||
# recoverable without restarting services that are already current.
|
# recoverable without restarting services that are already current.
|
||||||
ssh "$REMOTE" bash -s -- "${REMOTE_ARGS[@]}" <<'REMOTE_EOF'
|
ssh "$REMOTE" "$REMOTE_SHELL" -s -- "${REMOTE_ARGS[@]}" <<'REMOTE_EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TARGET="$1"
|
TARGET="$1"
|
||||||
EXPECTED_REVISION="$2"
|
EXPECTED_REVISION="$2"
|
||||||
shift 2
|
REPO_DIR="$3"
|
||||||
cd "/home/dank/serverAppConfigs"
|
REMOTE_SHELL="$4"
|
||||||
|
shift 4
|
||||||
|
export REMOTE_DIR="$REPO_DIR"
|
||||||
|
cd "$REPO_DIR"
|
||||||
OLD_REVISION="$(git rev-parse HEAD)"
|
OLD_REVISION="$(git rev-parse HEAD)"
|
||||||
echo " pulling origin/main..."
|
echo " pulling origin/main..."
|
||||||
git pull --ff-only origin main
|
git pull --ff-only origin main
|
||||||
@@ -210,7 +230,7 @@ if [[ "$NEW_REVISION" != "$EXPECTED_REVISION" ]]; then
|
|||||||
echo "FAIL: target checkout is $NEW_REVISION, expected $EXPECTED_REVISION" >&2
|
echo "FAIL: target checkout is $NEW_REVISION, expected $EXPECTED_REVISION" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
exec ./scripts/deploy-remote.sh "$TARGET" "$NEW_REVISION" "$@" \
|
exec "$REMOTE_SHELL" ./scripts/deploy-remote.sh "$TARGET" "$NEW_REVISION" "$@" \
|
||||||
--from-revision "$OLD_REVISION"
|
--from-revision "$OLD_REVISION"
|
||||||
REMOTE_EOF
|
REMOTE_EOF
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,14 @@ for path in "${CHANGED[@]}"; do
|
|||||||
add_test tests/deployment_flow.bats
|
add_test tests/deployment_flow.bats
|
||||||
add_test tests/deployment_scoping.bats
|
add_test tests/deployment_scoping.bats
|
||||||
;;
|
;;
|
||||||
|
# FrankBook-Pro units live inside the podman machine VM; the seam
|
||||||
|
# pins boot persistence, the gluetun netns invariant, and the
|
||||||
|
# plan-locked volume layout. Deployer changes route here too.
|
||||||
|
frankbook-pro/*)
|
||||||
|
add_test tests/frankbook_quadlet.bats
|
||||||
|
add_test tests/deployment_flow.bats
|
||||||
|
add_test tests/deployment_scoping.bats
|
||||||
|
;;
|
||||||
report/*)
|
report/*)
|
||||||
# Frozen audit reports are point-in-time artifacts; not testable.
|
# Frozen audit reports are point-in-time artifacts; not testable.
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -111,6 +111,17 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|||||||
[ "$output" = $'gitea-postgres\ngitea-server' ]
|
[ "$output" = $'gitea-postgres\ngitea-server' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "frankbook-pro gluetun changes carry qbittorrent; arr changes stay scoped" {
|
||||||
|
run python3 "$PLANNER" --target frankbook-pro --paths \
|
||||||
|
frankbook-pro/gluetun/quadlet/gluetun.container
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = $'gluetun\nqbittorrent' ]
|
||||||
|
run python3 "$PLANNER" --target frankbook-pro --paths \
|
||||||
|
frankbook-pro/sonarr/quadlet/sonarr.container
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ "$output" = "sonarr" ]
|
||||||
|
}
|
||||||
|
|
||||||
@test "remote Caddy deployment no longer blocks on the retired Gitea proxy" {
|
@test "remote Caddy deployment no longer blocks on the retired Gitea proxy" {
|
||||||
! grep -q 'refusing to replace it with caddy' "$REMOTE_DEPLOY"
|
! grep -q 'refusing to replace it with caddy' "$REMOTE_DEPLOY"
|
||||||
! grep -q 'podman container exists gitea-caddy &&' "$REMOTE_DEPLOY"
|
! grep -q 'podman container exists gitea-caddy &&' "$REMOTE_DEPLOY"
|
||||||
|
|||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
# Seam: the FrankBook-Pro quadlet units (gluetun, qbittorrent, five *arrs).
|
||||||
|
# These units are installed INSIDE the Mac's podman machine VM by the
|
||||||
|
# frankbook-pro handler in deploy-remote.sh. The contract pinned here:
|
||||||
|
# boot persistence, self-healing, the gluetun netns invariant (qBittorrent
|
||||||
|
# publishes no port of its own; gluetun publishes both), the verbatim
|
||||||
|
# plan-locked data-root mounts, and zero literal secrets in tracked files.
|
||||||
|
|
||||||
|
load helpers
|
||||||
|
|
||||||
|
FRANKBOOK_UNITS=(
|
||||||
|
gluetun/quadlet/gluetun
|
||||||
|
qbittorrent/quadlet/qbittorrent
|
||||||
|
prowlarr/quadlet/prowlarr
|
||||||
|
sonarr/quadlet/sonarr
|
||||||
|
radarr/quadlet/radarr
|
||||||
|
lidarr/quadlet/lidarr
|
||||||
|
readarr/quadlet/readarr
|
||||||
|
)
|
||||||
|
|
||||||
|
@test "all seven frankbook-pro units exist under frankbook-pro/<svc>/quadlet/" {
|
||||||
|
for unit in "${FRANKBOOK_UNITS[@]}"; do
|
||||||
|
[ -f "$REPO_ROOT/frankbook-pro/$unit.container" ] || return 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "every frankbook-pro unit is boot-persistent and self-healing" {
|
||||||
|
for unit in "${FRANKBOOK_UNITS[@]}"; do
|
||||||
|
grep -q '^WantedBy=default.target$' "$REPO_ROOT/frankbook-pro/$unit.container" || return 1
|
||||||
|
grep -q '^Restart=always$' "$REPO_ROOT/frankbook-pro/$unit.container" || return 1
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "qbittorrent joins gluetun's netns and publishes no port of its own" {
|
||||||
|
local unit="$REPO_ROOT/frankbook-pro/qbittorrent/quadlet/qbittorrent.container"
|
||||||
|
grep -q '^Network=gluetun.container$' "$unit" || return 1
|
||||||
|
grep -q '^Wants=gluetun.service$' "$unit" || return 1
|
||||||
|
grep -q '^After=gluetun.service$' "$unit" || return 1
|
||||||
|
! grep -q '^PublishPort=' "$unit" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "gluetun publishes qbittorrent's WebUI and torrent ports and needs NET_ADMIN+tun" {
|
||||||
|
local unit="$REPO_ROOT/frankbook-pro/gluetun/quadlet/gluetun.container"
|
||||||
|
grep -q '^PublishPort=8080:8080$' "$unit" || return 1
|
||||||
|
grep -q '^PublishPort=6881:6881$' "$unit" || return 1
|
||||||
|
grep -q '^PublishPort=6881:6881/udp$' "$unit" || return 1
|
||||||
|
grep -q '^AddCapability=NET_ADMIN$' "$unit" || return 1
|
||||||
|
grep -q '^AddDevice=/dev/net/tun:/dev/net/tun$' "$unit" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "gluetun credentials come from a machine-local env file, never literals" {
|
||||||
|
local unit="$REPO_ROOT/frankbook-pro/gluetun/quadlet/gluetun.container"
|
||||||
|
grep -q '^EnvironmentFile=/home/core/Containers/gluetun/env$' "$unit" || return 1
|
||||||
|
# Only ${VAR} expansions may carry the WireGuard material.
|
||||||
|
if grep -qE '^Environment=WIREGUARD_PRIVATE_KEY=[^$]' "$unit"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "data roots follow the plan-locked virtiofs layout" {
|
||||||
|
for svc in qbittorrent prowlarr sonarr radarr lidarr readarr; do
|
||||||
|
grep -q "^Volume=/Volumes/COMPLETE/$svc-data:/config$" \
|
||||||
|
"$REPO_ROOT/frankbook-pro/$svc/quadlet/$svc.container" || return 1
|
||||||
|
done
|
||||||
|
# qBittorrent additionally mounts both external disks verbatim: the
|
||||||
|
# seeded conf references the literal /Volumes paths.
|
||||||
|
local unit="$REPO_ROOT/frankbook-pro/qbittorrent/quadlet/qbittorrent.container"
|
||||||
|
grep -q '^Volume=/Volumes/COMPLETE:/Volumes/COMPLETE$' "$unit" || return 1
|
||||||
|
grep -q '^Volume=/Volumes/INCOMPLETE:/Volumes/INCOMPLETE$' "$unit" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "every arr serves its standard port and readarr keeps its pinned image" {
|
||||||
|
grep -q '^PublishPort=9696:9696$' "$REPO_ROOT/frankbook-pro/prowlarr/quadlet/prowlarr.container" || return 1
|
||||||
|
grep -q '^PublishPort=8989:8989$' "$REPO_ROOT/frankbook-pro/sonarr/quadlet/sonarr.container" || return 1
|
||||||
|
grep -q '^PublishPort=7878:7878$' "$REPO_ROOT/frankbook-pro/radarr/quadlet/radarr.container" || return 1
|
||||||
|
grep -q '^PublishPort=8686:8686$' "$REPO_ROOT/frankbook-pro/lidarr/quadlet/lidarr.container" || return 1
|
||||||
|
grep -q '^PublishPort=8787:8787$' "$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1
|
||||||
|
grep -q '^Image=lscr.io/linuxserver/readarr:amd64-1.0.980$' \
|
||||||
|
"$REPO_ROOT/frankbook-pro/readarr/quadlet/readarr.container" || return 1
|
||||||
|
}
|
||||||
Reference in new issue
Block a user