T-HUB/infra/peertube-workers/install-peertube-worker.sh
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

196 lines
11 KiB
Bash
Executable file

#!/bin/bash
# Native Debian 12 / Ubuntu 24.04+ Intel DRM worker. No secrets in argv/history.
set -euo pipefail
set +x
umask 077
task_source=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
worker_name= peertube_url= token_fd= canonical_origin= tailscale_peer=TAILSCALE_IP
while (($#)); do
case "$1" in
--name) worker_name=${2:?}; shift 2 ;;
--peertube-url) peertube_url=${2:?}; shift 2 ;;
--tailscale-peer) tailscale_peer=${2:?}; shift 2 ;;
--canonical-origin) canonical_origin=${2:?}; shift 2 ;;
--registration-token-fd) token_fd=${2:?}; shift 2 ;;
*) echo 'Usage: install-peertube-worker.sh --name HOST --peertube-url URL [--tailscale-peer IP] [--registration-token-fd N]' >&2; exit 2 ;;
esac
done
[[ $EUID -eq 0 && $worker_name =~ ^[a-zA-Z0-9._-]+$ && $peertube_url =~ ^https?:// ]] || { echo 'Root, valid name and URL required' >&2; exit 2; }
exec 9>/run/lock/pt-worker-install.lock
flock -n 9 || { echo 'Another installation is running' >&2; exit 1; }
[[ $(ps -p 1 -o comm=) == systemd ]] || { echo 'systemd is required; inspect WSL init before onboarding' >&2; exit 1; }
[[ -c /dev/dri/renderD128 ]] || { echo 'No Intel DRM render device; /dev/dxg alone is not this Linux QSV path. No packages or services changed.' >&2; exit 1; }
[[ $(cat /sys/class/drm/renderD128/device/vendor) == 0x8086 ]] || { echo 'This installer needs an Intel GPU; NVIDIA needs another hardware adapter' >&2; exit 1; }
[[ $(basename "$(readlink -f /sys/class/drm/renderD128/device/driver)") == i915 || $(basename "$(readlink -f /sys/class/drm/renderD128/device/driver)") == xe ]] || { echo 'Intel DRM driver unavailable' >&2; exit 1; }
command -v tailscale >/dev/null || { echo 'Tailscale must be joined already' >&2; exit 1; }
tailscale ping --c=1 --timeout=5s "$tailscale_peer" >/dev/null
canonical_origin=$(python3 - "$peertube_url" "$canonical_origin" <<'PY'
import json, sys, urllib.request
from urllib.parse import urlsplit
with urllib.request.urlopen(sys.argv[1].rstrip('/')+'/api/v1/config', timeout=10) as r:
c=json.load(r); v=c['serverVersion']
if v != '7.3.0': raise SystemExit('This adapter is verified against PeerTube 7.3.0; inspect a different version before installing')
if sys.version_info < (3,11): raise SystemExit('Python >=3.11 required')
origin=sys.argv[2]
if not origin:
logos=[x['fileUrl'] for x in c['instance']['logo'] if x.get('isFallback')]
if not logos: raise SystemExit('Supply --canonical-origin: no fallback asset URL identifies the instance origin')
u=urlsplit(logos[0]);origin=u.scheme+'://'+u.netloc
print(origin.rstrip('/'))
PY
)
if [[ -f /etc/pt-worker/worker.json ]]; then
python3 - "$worker_name" "$peertube_url" <<'PY'
import json, sys
c=json.load(open('/etc/pt-worker/worker.json'))
if c['name'] != sys.argv[1] or c['peertube_url'].rstrip('/') != sys.argv[2].rstrip('/'):
raise SystemExit('Existing name/URL differs: preserve registration and inspect before changing it')
PY
fi
if systemctl is-active --quiet pt-worker.service; then
/usr/local/bin/pt-worker drain
for _ in $(seq 1 120); do
if /usr/local/bin/pt-worker status | python3 -c 'import json,sys;sys.exit(bool(json.load(sys.stdin).get("runner_running")))'; then break; fi
sleep 5
done
/usr/local/bin/pt-worker status | python3 -c 'import json,sys;sys.exit(bool(json.load(sys.stdin).get("runner_running")))' || { echo 'Active jobs still draining; installer stopped without overwriting code' >&2; exit 1; }
systemctl stop pt-worker.service
fi
if [[ -d /opt/pt-worker ]]; then
mkdir -p /var/backups/pt-worker
task_backup=/var/backups/pt-worker/$(date -u +%Y%m%dT%H%M%SZ).tgz
tar -czf "$task_backup" /opt/pt-worker /etc/pt-worker /var/lib/pt-worker/.config
chmod 600 "$task_backup"
fi
. /etc/os-release
case "$ID" in debian|ubuntu) ;; *) echo 'Only Debian/Ubuntu package sets verified' >&2; exit 1;; esac
apt-get update
# Intel media VA driver includes Alder Lake support; do not change kernel/perf policies.
task_media_package=intel-media-va-driver
if apt-cache show intel-media-va-driver-non-free >/dev/null 2>&1; then task_media_package=intel-media-va-driver-non-free; fi
apt-get install -y ffmpeg vainfo "$task_media_package" intel-gpu-tools python3 curl ca-certificates xz-utils pciutils
python3 - <<'PY'
import re, subprocess
s=subprocess.check_output(['/usr/bin/ffmpeg','-version'],text=True)
m=re.search(r'ffmpeg version (\d+)\.(\d+)',s)
if not m or tuple(map(int,m.groups())) < (4,3): raise SystemExit('FFmpeg >=4.3 required')
subprocess.run(['/usr/bin/ffprobe','-version'],check=True,stdout=subprocess.DEVNULL)
PY
id pt-worker >/dev/null 2>&1 || useradd --system --create-home --home-dir /var/lib/pt-worker --shell /usr/sbin/nologin pt-worker
for task_group in render video; do
getent group "$task_group" >/dev/null && usermod -a -G "$task_group" pt-worker
done
task_render_group=$(stat -c %G /dev/dri/renderD128)
[[ $task_render_group != root ]] || { echo 'Render device has root group; fix the host udev policy first' >&2; exit 1; }
usermod -a -G "$task_render_group" pt-worker
install -d -m 755 /opt/pt-worker
install -d -m 750 -o root -g pt-worker /etc/pt-worker
install -d -m 700 -o pt-worker -g pt-worker /var/lib/pt-worker/state
for task_file in pt-worker.py ffmpeg-qsv.py observe.cjs ipc.cjs; do
install -m 755 "$task_source/$task_file" "/opt/pt-worker/$task_file"
done
install -m 755 "$task_source/pt-worker" /usr/local/bin/pt-worker
if [[ ! -x /opt/pt-worker/node/bin/node ]]; then
task_tmp=$(mktemp -d)
trap 'python3 -c '\''import shutil,sys;shutil.rmtree(sys.argv[1])'\'' "$task_tmp"' EXIT
case "$(uname -m)" in x86_64) task_arch=x64;; *) echo 'Intel worker requires x86_64' >&2; exit 1;; esac
curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt -o "$task_tmp/SHA256SUMS"
task_archive=$(awk '$2 ~ /linux-x64.tar.xz$/ {print $2}' "$task_tmp/SHA256SUMS")
[[ $task_archive =~ ^node-v22\.[0-9]+\.[0-9]+-linux-x64.tar.xz$ ]] || exit 1
curl -fsSL "https://nodejs.org/dist/latest-v22.x/$task_archive" -o "$task_tmp/$task_archive"
(cd "$task_tmp"; awk -v file="$task_archive" '$2==file' SHA256SUMS | sha256sum -c -)
mkdir -p /opt/pt-worker/node
tar -xJf "$task_tmp/$task_archive" -C /opt/pt-worker/node --strip-components=1
fi
export PATH=/opt/pt-worker/node/bin:$PATH
node -e 'if (+process.versions.node.split(".")[0] < 22) process.exit(1)'
install -d -m 755 /opt/pt-worker/runtime
install -m 644 "$task_source/runtime-package.json" /opt/pt-worker/runtime/package.json
install -m 644 "$task_source/runtime-package-lock.json" /opt/pt-worker/runtime/package-lock.json
npm ci --prefix /opt/pt-worker/runtime --omit=optional --ignore-scripts --no-audit --no-fund
[[ $(/opt/pt-worker/runtime/node_modules/.bin/peertube-runner --version) == 0.2.0 ]] || exit 1
if [[ ! -f /etc/pt-worker/worker.json ]]; then
python3 - "$task_source/worker.example.json" "$worker_name" "$peertube_url" "$tailscale_peer" "$canonical_origin" <<'PY'
import json,sys
c=json.load(open(sys.argv[1]));c.update(name=sys.argv[2],peertube_url=sys.argv[3].rstrip('/'),tailscale_peer=sys.argv[4],canonical_origin=sys.argv[5],probe_url='http://'+sys.argv[4]+':9044')
with open('/etc/pt-worker/worker.json','w') as f: json.dump(c,f,indent=2)
PY
fi
chown root:pt-worker /etc/pt-worker/worker.json
chmod 640 /etc/pt-worker/worker.json
# Real hardware test as the actual service user, with its real device permissions.
/usr/local/bin/pt-worker self-test || { echo 'QSV failed or media-engine evidence unavailable: not registering or starting' >&2; exit 1; }
task_registered=$(runuser -u pt-worker -- python3 - <<'PY'
import tomllib,json
from pathlib import Path
c=json.load(open('/etc/pt-worker/worker.json')); p=Path('/var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml')
d=tomllib.loads(p.read_text()) if p.exists() else {}
print(int(any(x['url'].rstrip('/')==c['peertube_url'] and x['runnerName']==c['name'] for x in d.get('registeredInstances',[]))))
PY
)
if [[ $task_registered == 0 ]]; then
[[ ! -e /var/lib/pt-worker/state/enrollment.pending ]] || { echo 'Previous registration outcome is uncertain; inspect the named runner in PeerTube before retrying. No duplicate enrollment attempted.' >&2; exit 1; }
# Registration requires a running server. concurrency=0 makes enrollment refuse jobs.
install -d -m 700 -o pt-worker -g pt-worker /var/lib/pt-worker/.config/peertube-runner-nodejs/default
if [[ -e /var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml ]]; then
python3 - <<'PY'
import tomllib
from pathlib import Path
p=Path('/var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml')
c=tomllib.loads(p.read_text())
if c.get('registeredInstances'): raise SystemExit('Other registered instances exist: inspect before enrollment')
PY
fi
cat > /var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml <<'TOML'
[jobs]
concurrency = 0
[ffmpeg]
threads = 2
nice = 20
TOML
chown pt-worker:pt-worker /var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml
# Logs go to /dev/null during enrollment; the official CLI secret flag is deliberately avoided.
runuser -u pt-worker -- env HOME=/var/lib/pt-worker PATH="$PATH" \
/opt/pt-worker/runtime/node_modules/.bin/peertube-runner server \
--enable-job vod-web-video-transcoding --enable-job vod-hls-transcoding --enable-job vod-audio-merge-transcoding >/dev/null 2>&1 &
task_enroll_pid=$!
trap 'kill "$task_enroll_pid" 2>/dev/null || true; [[ -z ${task_tmp:-} ]] || python3 -c '\''import shutil,sys;shutil.rmtree(sys.argv[1])'\'' "$task_tmp"' EXIT
for _ in $(seq 1 30); do
[[ -S /var/lib/pt-worker/.local/share/peertube-runner-nodejs/default/peertube-runner.sock ]] && break
sleep 1
done
if [[ -n $token_fd ]]; then
[[ $token_fd =~ ^[0-9]+$ ]] || exit 2
touch /var/lib/pt-worker/state/enrollment.pending
runuser -u pt-worker -- env HOME=/var/lib/pt-worker /opt/pt-worker/node/bin/node /opt/pt-worker/ipc.cjs register <&"$token_fd"
else
[[ -t 0 ]] || { echo 'Provide a secure token FD or run interactively' >&2; exit 1; }
read -r -s -p 'Dedicated runner registration token: ' task_token
echo
touch /var/lib/pt-worker/state/enrollment.pending
printf '%s' "$task_token" | runuser -u pt-worker -- env HOME=/var/lib/pt-worker /opt/pt-worker/node/bin/node /opt/pt-worker/ipc.cjs register
unset task_token
fi
runuser -u pt-worker -- env HOME=/var/lib/pt-worker PATH="$PATH" /opt/pt-worker/runtime/node_modules/.bin/peertube-runner graceful-shutdown >/dev/null
wait "$task_enroll_pid"
# IPC returns success only after saving the dedicated runner credential to TOML.
fi
python3 - <<'PY'
from pathlib import Path
Path('/var/lib/pt-worker/state/enrollment.pending').unlink(missing_ok=True)
PY
python3 - <<'PY'
import re,json
from pathlib import Path
p=Path('/var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml')
c=json.load(open('/etc/pt-worker/worker.json'))
s=p.read_text(); s,n=re.subn(r'(?m)^concurrency\s*=\s*\d+', 'concurrency = '+str(c['concurrency']), s,count=1)
if n!=1: raise SystemExit('Missing concurrency configuration')
p.write_text(s)
PY
chmod 600 /var/lib/pt-worker/.config/peertube-runner-nodejs/default/config.toml
install -m 644 "$task_source/systemd/pt-worker.service" /etc/systemd/system/pt-worker.service
systemctl daemon-reload
systemctl enable --now pt-worker.service
echo 'Registered. Admission starts PAUSED; measure bandwidth or use pt-worker start --calibrate for initial real jobs.'
/usr/local/bin/pt-worker status