Bootstrap tree for the media box Linux conversion
Autoinstall lays down a thin base (sshd, key, DHCP, Docker CE, /srv) and hands
off to this repo on first boot. Everything interesting stays in git so it is
reviewable and re-runnable, rather than frozen onto a USB nobody can diff.
Stages, all idempotent:
00-preflight asserts hardware/BIOS state, changes nothing. Catches a BIOS
update having silently re-enabled Secure Boot, which would stop
the NVIDIA DKMS module loading on a box with no keyboard.
10-secrets ADD-ONLY seeder for /srv/secrets/stacks.env. Never overwrites an
existing key. Verified against a pre-populated file: existing
values, unrelated keys, the operator tier and existing manifest
lines all survive byte-for-byte; a second run is a no-op.
20-cifs the 8 shares Plex actually uses (Share is excluded, it is not a
library root). \040 escaping, nofail + x-systemd.automount +
_netdev. Managed-block rewrite verified not to duplicate or to
drop the root fstab entry.
30-nvidia nvidia-driver-580 explicitly: 580 is the LAST branch supporting
Pascal, and the -open modules need Turing+. Pins against newer
branches. Not in late-commands because DKMS needs the installed
kernel, not the installer's.
40-shell-mcp builds the native MCP locally for amd64; refuses to finish
unless /sse returns 401 without a token.
50-plex run by hand: PLEX_CLAIM expires in 4 minutes. Refuses to start
against missing mounts and disables autoEmptyTrash, which with
read-write NAS credentials is the most dangerous default here.
shell-mcp was built on arm64 originally. It builds clean on amd64 (whole dep
tree resolves to prebuilt manylinux x86_64 wheels, no compiler needed), but
dependencies are now pinned - the original installed mcp/starlette/uvicorn
unpinned and starlette has since gone 1.x. Port moved to 8103 so NPM host 42
can simply be repointed, and the tool description now says media box rather
than arrsstack.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 00-preflight — assert the box is what we think it is before changing it.
|
||||
#
|
||||
# This stage NEVER modifies anything. It only reports. Its job is to catch
|
||||
# "the BIOS update reset your settings" before you spend an hour wondering
|
||||
# why Plex will not transcode.
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
RC=0
|
||||
note() { printf ' %-34s %s\n' "$1" "$2"; }
|
||||
bad() { printf ' \033[1;31m%-34s %s\033[0m\n' "$1" "$2"; RC=1; }
|
||||
warn() { printf ' \033[1;33m%-34s %s\033[0m\n' "$1" "$2"; }
|
||||
|
||||
echo "--- identity ---"
|
||||
note "hostname" "$(hostname)"
|
||||
note "kernel" "$(uname -r)"
|
||||
note "ip" "$(hostname -I | tr -s ' ')"
|
||||
|
||||
echo "--- firmware ---"
|
||||
if [ -d /sys/firmware/efi ]; then note "boot mode" "UEFI"; else bad "boot mode" "LEGACY/BIOS — expected UEFI"; fi
|
||||
|
||||
# Secure Boot must stay OFF. A BIOS update commonly restores defaults, and the
|
||||
# ASUS default for this board turns Secure Boot back on. With it on, the DKMS
|
||||
# NVIDIA module will not load and there is no keyboard attached to enroll a MOK.
|
||||
if command -v mokutil >/dev/null 2>&1; then
|
||||
SB="$(mokutil --sb-state 2>/dev/null || echo unknown)"
|
||||
case "$SB" in
|
||||
*disabled*) note "secure boot" "disabled (correct)" ;;
|
||||
*enabled*) bad "secure boot" "ENABLED — NVIDIA DKMS will not load. Disable it in BIOS." ;;
|
||||
*) warn "secure boot" "$SB" ;;
|
||||
esac
|
||||
else
|
||||
warn "secure boot" "mokutil not installed; check BIOS manually"
|
||||
fi
|
||||
|
||||
echo "--- disks ---"
|
||||
# Disk 0 must be the system disk. Disk 1 must still be NTFS and untouched.
|
||||
SYS_SERIAL="$(lsblk -dno SERIAL "$(findmnt -no SOURCE / | sed -E 's/p?[0-9]+$//')" 2>/dev/null | tr -d ' ')"
|
||||
if [ "$SYS_SERIAL" = "1808AE802176" ]; then
|
||||
note "root disk serial" "1808AE802176 (correct)"
|
||||
else
|
||||
bad "root disk serial" "${SYS_SERIAL:-unknown} — expected 1808AE802176"
|
||||
fi
|
||||
|
||||
DATA_DEV="$(lsblk -dno NAME,SERIAL | awk '$2=="WD-WCC7K3JAEDR3"{print $1}')"
|
||||
if [ -n "$DATA_DEV" ]; then
|
||||
FSTYPES="$(lsblk -no FSTYPE "/dev/$DATA_DEV" | tr -s '\n' ' ')"
|
||||
note "data disk (D:)" "/dev/$DATA_DEV present, fstypes: ${FSTYPES:-none}"
|
||||
if echo "$FSTYPES" | grep -q ntfs; then
|
||||
note "data disk state" "still NTFS — correct, leave it until after soak"
|
||||
else
|
||||
warn "data disk state" "no NTFS found — has it already been converted?"
|
||||
fi
|
||||
if findmnt -rno TARGET -S "/dev/${DATA_DEV}1" >/dev/null 2>&1; then
|
||||
warn "data disk mounted" "D: is mounted; it should not be during the soak"
|
||||
fi
|
||||
else
|
||||
bad "data disk (D:)" "WD-WCC7K3JAEDR3 NOT FOUND"
|
||||
fi
|
||||
|
||||
echo "--- gpu ---"
|
||||
if lspci -nn | grep -qi 'VGA.*Intel'; then
|
||||
note "iGPU (UHD 630)" "present"
|
||||
else
|
||||
bad "iGPU (UHD 630)" "NOT enumerated — set BIOS: Advanced > System Agent (SA) Configuration > Graphics Configuration > iGPU Multi-Monitor = Enabled"
|
||||
fi
|
||||
if lspci -nn | grep -qi 'NVIDIA'; then
|
||||
note "GTX 1070" "$(lspci -nn | grep -i nvidia | head -1 | cut -c1-70)"
|
||||
else
|
||||
warn "GTX 1070" "not seen on PCI bus"
|
||||
fi
|
||||
if [ -e /dev/dri/renderD128 ]; then
|
||||
note "/dev/dri/renderD128" "present"
|
||||
note "render group gid" "$(stat -c '%g (%G)' /dev/dri/renderD128)"
|
||||
else
|
||||
warn "/dev/dri/renderD128" "missing — Quick Sync unavailable until iGPU is enabled in BIOS"
|
||||
fi
|
||||
|
||||
echo "--- runtime ---"
|
||||
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
||||
note "docker" "$(docker --version | cut -d, -f1)"
|
||||
else
|
||||
bad "docker" "not running"
|
||||
fi
|
||||
note "media uid/gid" "$(id -u media 2>/dev/null || echo '?'):$(getent group media | cut -d: -f3 2>/dev/null || echo '?')"
|
||||
note "swap" "$(free -h | awk '/Swap:/{print $2}')"
|
||||
|
||||
echo
|
||||
[ $RC -eq 0 ] && echo " preflight clean" || echo " preflight found problems above"
|
||||
# Preflight never blocks the rest of the bootstrap — it reports.
|
||||
exit 0
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 10-secrets — idempotent, ADD-ONLY seeder for /srv/secrets/stacks.env
|
||||
#
|
||||
# Mirrors the arrsstack convention: one master file, three tiers.
|
||||
# [OPERATOR] secrets for operating the host; never emitted to a container
|
||||
# [VALUES] flat, deduplicated KEY=value
|
||||
# [MANIFEST] "#@stack <name> = VAR VAR" lines consumed by genenv.sh
|
||||
#
|
||||
# GUARANTEES:
|
||||
# * An existing key is NEVER touched. Not its value, not its position,
|
||||
# not its comment. If KEY= exists, this script skips it entirely.
|
||||
# * A missing key is appended with either a generated value (for tokens we
|
||||
# can safely generate) or the literal FILL_ME (for anything a human must
|
||||
# supply). genenv.sh already fails closed on FILL_ME.
|
||||
# * Values are never printed. Only key names and add/skip decisions.
|
||||
# * Running this twice in a row produces zero changes the second time.
|
||||
#
|
||||
# This is a SEPARATE secrets file from arrsstack's. Per the project decision:
|
||||
# no shared or mounted secrets between hosts. Canonical copy is Vaultwarden.
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
MASTER="/srv/secrets/stacks.env"
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
umask 077
|
||||
install -d -m 0700 /srv/secrets
|
||||
|
||||
added=0; skipped=0
|
||||
|
||||
# --- create the skeleton only if the file does not exist at all -------------
|
||||
if [ ! -f "$MASTER" ]; then
|
||||
echo " creating $MASTER from template"
|
||||
install -m 0600 "$REPO_DIR/secrets/stacks.env.example" "$MASTER"
|
||||
fi
|
||||
|
||||
# Ensure the tier markers exist, so insertion has something to anchor to.
|
||||
grep -q '^# ----- \[VALUES\]' "$MASTER" || printf '\n# ----- [VALUES] -----\n' >> "$MASTER"
|
||||
grep -q '^# ----- \[MANIFEST\]' "$MASTER" || printf '\n# ----- [MANIFEST] -----\n' >> "$MASTER"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# add_value <KEY> <generator>
|
||||
# generator: "hex32" -> openssl rand -hex 32
|
||||
# "fill" -> literal FILL_ME (human must supply)
|
||||
# anything else -> used as the literal default value
|
||||
# Inserts immediately BEFORE the [MANIFEST] marker so it lands inside [VALUES].
|
||||
# ---------------------------------------------------------------------------
|
||||
add_value() {
|
||||
local key="$1" gen="$2" val
|
||||
|
||||
if grep -qE "^${key}=" "$MASTER"; then
|
||||
printf ' skip %-32s (already present)\n' "$key"
|
||||
skipped=$((skipped+1))
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$gen" in
|
||||
hex32) val="$(openssl rand -hex 32)" ;;
|
||||
fill) val="FILL_ME" ;;
|
||||
*) val="$gen" ;;
|
||||
esac
|
||||
|
||||
# Insert before the [MANIFEST] header, preserving everything else byte-for-byte.
|
||||
local tmp; tmp="$(mktemp)"
|
||||
awk -v line="${key}=${val}" '
|
||||
/^# ----- \[MANIFEST\]/ && !done { print line; print ""; done=1 }
|
||||
{ print }
|
||||
' "$MASTER" > "$tmp"
|
||||
install -m 0600 "$tmp" "$MASTER"; rm -f "$tmp"
|
||||
|
||||
printf ' ADD %-32s (%s)\n' "$key" "$([ "$gen" = fill ] && echo 'needs a human' || echo generated)"
|
||||
added=$((added+1))
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# add_manifest <stack> <vars...>
|
||||
# Appends a "#@stack" line only if one does not already exist for that stack.
|
||||
# ---------------------------------------------------------------------------
|
||||
add_manifest() {
|
||||
local stack="$1"; shift
|
||||
if grep -qE "^#@stack[[:space:]]+${stack}[[:space:]]*=" "$MASTER"; then
|
||||
printf ' skip %-32s (manifest present)\n' "#@stack ${stack}"
|
||||
skipped=$((skipped+1))
|
||||
return 0
|
||||
fi
|
||||
printf '#@stack %s = %s\n' "$stack" "$*" >> "$MASTER"
|
||||
printf ' ADD %-32s\n' "#@stack ${stack}"
|
||||
added=$((added+1))
|
||||
}
|
||||
|
||||
echo " seeding $MASTER (add-only)"
|
||||
|
||||
# --- [VALUES] ---------------------------------------------------------------
|
||||
add_value TZ "America/New_York"
|
||||
# Reuses the SAME token value that arrsstack's mediabox-mcp already serves, so
|
||||
# the Claude connector entry does not change when NPM host 42 is repointed.
|
||||
# Left as FILL_ME: copy it across by hand rather than minting a new one.
|
||||
add_value MEDIABOX_MCP_BEARER_TOKEN fill
|
||||
add_value PLEX_ADVERTISE_URL "http://10.0.1.20:32400"
|
||||
|
||||
# --- [MANIFEST] -------------------------------------------------------------
|
||||
add_manifest shell-mcp MEDIABOX_MCP_BEARER_TOKEN TZ
|
||||
add_manifest plex TZ PLEX_ADVERTISE_URL
|
||||
|
||||
chmod 600 "$MASTER"
|
||||
chown root:root "$MASTER"
|
||||
|
||||
echo " result: ${added} added, ${skipped} left untouched"
|
||||
|
||||
# Report unfilled keys by NAME only — never values.
|
||||
if grep -qE '^[A-Za-z_][A-Za-z0-9_]*=FILL_ME$' "$MASTER"; then
|
||||
echo
|
||||
echo " keys still needing a value (fill by hand, then re-run):"
|
||||
grep -E '^[A-Za-z_][A-Za-z0-9_]*=FILL_ME$' "$MASTER" | cut -d= -f1 | sed 's/^/ - /'
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 20-cifs — NAS mounts for the 8 shares Plex actually uses.
|
||||
#
|
||||
# Shares confirmed live from the Windows box 2026-07-27 (exact names/casing):
|
||||
# Audio Books · Education Videos · Health · Home Movies · media
|
||||
# Music Organized · Pictures · Radio Shows (+ "Share" — NOT mounted)
|
||||
#
|
||||
# "Share" is mounted on Windows today but is not a Plex library root, so it is
|
||||
# deliberately left out. Every entry below maps to a real library section.
|
||||
#
|
||||
# 11 Plex library roots resolve onto these 8 mounts:
|
||||
# Audio Books -> /mnt/nas/audiobooks
|
||||
# Music Organized -> /mnt/nas/music-organized
|
||||
# Radio Shows -> /mnt/nas/radio-shows
|
||||
# Pictures -> /mnt/nas/pictures/Plex Pictures
|
||||
# Health -> /mnt/nas/health
|
||||
# Home Movies -> /mnt/nas/home-movies
|
||||
# Education Videos -> /mnt/nas/education-videos
|
||||
# media -> /mnt/nas/media/{movies,tvshows,music,audiobooks}
|
||||
#
|
||||
# WHY EACH OPTION IS THERE — none of these are decoration:
|
||||
# nofail an unreachable NAS must NOT drop a headless
|
||||
# box into an emergency shell. There is no
|
||||
# keyboard attached to type the root password.
|
||||
# _netdev tells systemd this needs the network up.
|
||||
# x-systemd.automount mount on first access rather than at boot, so
|
||||
# a slow NAS never stretches boot time.
|
||||
# x-systemd.mount-timeout=30 bounded failure instead of an indefinite hang.
|
||||
# x-systemd.idle-timeout=600 unmount when idle; keeps stale handles rare.
|
||||
# vers=3.1.1 dialect confirmed from the Windows box.
|
||||
# uid/gid=3000 MUST match Plex's PUID/PGID or every file is
|
||||
# permission-denied.
|
||||
# \040 fstab field separator is whitespace; share
|
||||
# names with spaces MUST escape them.
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
CRED="/etc/cifs/korval.cred"
|
||||
NAS="10.0.1.254"
|
||||
MEDIA_UID=3000
|
||||
MEDIA_GID=3000
|
||||
MARK_BEGIN="# >>> mediabox NAS mounts (managed by 20-cifs.sh) >>>"
|
||||
MARK_END="# <<< mediabox NAS mounts <<<"
|
||||
|
||||
OPTS="credentials=${CRED},vers=3.1.1,uid=${MEDIA_UID},gid=${MEDIA_GID},file_mode=0664,dir_mode=0775,iocharset=utf8,nofail,_netdev,x-systemd.automount,x-systemd.mount-timeout=30,x-systemd.idle-timeout=600"
|
||||
|
||||
# share-name-on-nas | local mount point
|
||||
SHARES=(
|
||||
"Audio Books|/mnt/nas/audiobooks"
|
||||
"Education Videos|/mnt/nas/education-videos"
|
||||
"Health|/mnt/nas/health"
|
||||
"Home Movies|/mnt/nas/home-movies"
|
||||
"media|/mnt/nas/media"
|
||||
"Music Organized|/mnt/nas/music-organized"
|
||||
"Pictures|/mnt/nas/pictures"
|
||||
"Radio Shows|/mnt/nas/radio-shows"
|
||||
)
|
||||
|
||||
# --- --verify mode: check mounts, change nothing ----------------------------
|
||||
if [ "${1:-}" = "--verify" ]; then
|
||||
echo " verifying NAS mounts"
|
||||
rc=0
|
||||
if [ ! -s "$CRED" ]; then
|
||||
echo " [FAIL] $CRED is empty — write the NAS credentials first"
|
||||
exit 1
|
||||
fi
|
||||
for entry in "${SHARES[@]}"; do
|
||||
mp="${entry#*|}"
|
||||
if ls "$mp" >/dev/null 2>&1 && mountpoint -q "$mp"; then
|
||||
printf ' [ok] %-28s %s\n' "$(basename "$mp")" "$(df -h --output=size "$mp" 2>/dev/null | tail -1 | tr -d ' ')"
|
||||
else
|
||||
printf ' [FAIL] %-28s not mounted\n' "$(basename "$mp")"
|
||||
rc=1
|
||||
fi
|
||||
done
|
||||
exit $rc
|
||||
fi
|
||||
|
||||
echo " writing fstab entries for ${#SHARES[@]} shares"
|
||||
|
||||
install -d -m 0700 /etc/cifs
|
||||
[ -f "$CRED" ] || install -m 0600 /dev/null "$CRED"
|
||||
chmod 600 "$CRED"
|
||||
|
||||
# The credentials file is created EMPTY by autoinstall on purpose. The real
|
||||
# password is written post-boot over the MCP so it never rides on the USB.
|
||||
if [ ! -s "$CRED" ]; then
|
||||
cat <<'CREDNOTE'
|
||||
NOTE: /etc/cifs/korval.cred is empty. That is expected at this stage.
|
||||
The mounts will fail (harmlessly, thanks to nofail) until you write:
|
||||
printf 'username=<user>\npassword=<pass>\ndomain=WORKGROUP\n' \
|
||||
> /etc/cifs/korval.cred
|
||||
chmod 600 /etc/cifs/korval.cred
|
||||
No quotes. No spaces around '='. Trailing newline required.
|
||||
CREDNOTE
|
||||
fi
|
||||
|
||||
for entry in "${SHARES[@]}"; do
|
||||
mp="${entry#*|}"
|
||||
install -d -m 0755 "$mp"
|
||||
chown ${MEDIA_UID}:${MEDIA_GID} "$mp"
|
||||
done
|
||||
|
||||
# Rebuild only our managed block; never touch the rest of fstab.
|
||||
tmp="$(mktemp)"
|
||||
awk -v b="$MARK_BEGIN" -v e="$MARK_END" '
|
||||
$0 == b { skip=1 }
|
||||
!skip { print }
|
||||
$0 == e { skip=0 }
|
||||
' /etc/fstab > "$tmp"
|
||||
|
||||
{
|
||||
printf '%s\n' "$MARK_BEGIN"
|
||||
for entry in "${SHARES[@]}"; do
|
||||
share="${entry%%|*}"
|
||||
mp="${entry#*|}"
|
||||
# escape spaces as \040 in BOTH fields (mount points here have none, but
|
||||
# the escaping is applied uniformly so a future renamed mount is safe)
|
||||
esc_share="${share// /\\040}"
|
||||
esc_mp="${mp// /\\040}"
|
||||
printf '//%s/%s %s cifs %s 0 0\n' "$NAS" "$esc_share" "$esc_mp" "$OPTS"
|
||||
done
|
||||
printf '%s\n' "$MARK_END"
|
||||
} >> "$tmp"
|
||||
|
||||
# Sanity: never install an fstab that lost the root entry.
|
||||
if ! awk '$2=="/" && $1 !~ /^#/' "$tmp" | grep -q .; then
|
||||
echo " [FAIL] refusing to write fstab — root entry missing from generated file"
|
||||
rm -f "$tmp"; exit 1
|
||||
fi
|
||||
|
||||
cp -a /etc/fstab "/etc/fstab.bak.$(date +%Y%m%d%H%M%S)"
|
||||
install -m 0644 "$tmp" /etc/fstab
|
||||
rm -f "$tmp"
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
echo " fstab updated (backup written alongside). Managed block:"
|
||||
sed -n "/${MARK_BEGIN//\//\\/}/,/${MARK_END//\//\\/}/p" /etc/fstab | sed 's/^/ /'
|
||||
|
||||
if [ -s "$CRED" ]; then
|
||||
echo " credentials present — attempting mounts"
|
||||
for entry in "${SHARES[@]}"; do
|
||||
mp="${entry#*|}"
|
||||
if timeout 40 mount "$mp" 2>/dev/null || mountpoint -q "$mp"; then
|
||||
printf ' [ok] %s\n' "$mp"
|
||||
else
|
||||
printf ' [warn] %s did not mount (check credentials / share name)\n' "$mp"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " skipping mount attempts until credentials are written"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 30-nvidia — NVIDIA driver + container toolkit for the GTX 1070
|
||||
#
|
||||
# WHY THIS IS NOT IN late-commands:
|
||||
# The driver is a DKMS module. It must build against the kernel that is
|
||||
# actually running on the installed system, with that kernel's headers, on a
|
||||
# real boot. Building it inside the installer environment produces a module
|
||||
# for the installer's kernel, which is not the kernel that boots.
|
||||
#
|
||||
# DRIVER BRANCH — this is the part that will bite later:
|
||||
# The GTX 1070 is Pascal. NVIDIA's 580 branch is the LAST branch that supports
|
||||
# Maxwell, Pascal and Volta; it is now a frozen legacy branch receiving
|
||||
# security fixes only. There will be no 590 for this card. Two consequences:
|
||||
# 1. We install nvidia-driver-580 explicitly. Never `ubuntu-drivers autoinstall`,
|
||||
# which will happily pick a newer branch that does not support the card.
|
||||
# 2. We install the PROPRIETARY module, not `-open`. The open kernel modules
|
||||
# require Turing or newer. On Pascal they will not load at all.
|
||||
#
|
||||
# Secure Boot must be OFF (verified 2026-07-27). With it on, the DKMS module is
|
||||
# unsigned as far as the firmware is concerned and requires interactive MOK
|
||||
# enrollment at a physical console — on a box with no keyboard.
|
||||
#
|
||||
# Plex is targeted at Quick Sync, not NVENC, so this stage failing does NOT
|
||||
# block Plex. It fails soft.
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
DRIVER_BRANCH=580
|
||||
|
||||
if ! lspci -nn | grep -qi nvidia; then
|
||||
echo " no NVIDIA device on the PCI bus — skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1; then
|
||||
echo " driver already working:"
|
||||
nvidia-smi --query-gpu=name,driver_version --format=csv,noheader | sed 's/^/ /'
|
||||
else
|
||||
echo " installing nvidia-driver-${DRIVER_BRANCH} (proprietary; Pascal cannot use -open)"
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
|
||||
# Explicit branch, explicit proprietary flavour. No ubuntu-drivers autoinstall.
|
||||
if ! apt-get install -y \
|
||||
"nvidia-driver-${DRIVER_BRANCH}" \
|
||||
"nvidia-utils-${DRIVER_BRANCH}" \
|
||||
"linux-headers-$(uname -r)" \
|
||||
dkms; then
|
||||
echo " [FAIL] driver install failed — Plex/Quick Sync is unaffected, fix later"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Persistence mode avoids a multi-second GPU init on every container start.
|
||||
systemctl enable --now nvidia-persistenced 2>/dev/null || true
|
||||
echo " driver installed — a REBOOT is required before nvidia-smi will work"
|
||||
fi
|
||||
|
||||
# --- NVIDIA Container Toolkit ------------------------------------------------
|
||||
if [ -f /etc/apt/sources.list.d/nvidia-container-toolkit.list ] \
|
||||
&& command -v nvidia-ctk >/dev/null 2>&1; then
|
||||
echo " container toolkit already present"
|
||||
else
|
||||
echo " installing NVIDIA container toolkit"
|
||||
install -m 0755 -d /usr/share/keyrings
|
||||
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
|
||||
| gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
|
||||
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
|
||||
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
|
||||
> /etc/apt/sources.list.d/nvidia-container-toolkit.list
|
||||
apt-get update -qq
|
||||
apt-get install -y nvidia-container-toolkit || {
|
||||
echo " [FAIL] container toolkit install failed"; exit 1; }
|
||||
|
||||
nvidia-ctk runtime configure --runtime=docker
|
||||
systemctl restart docker
|
||||
fi
|
||||
|
||||
# --- guard rail --------------------------------------------------------------
|
||||
# If a newer driver branch is ever pulled in, it will silently drop this card.
|
||||
cat > /etc/apt/preferences.d/nvidia-pascal.pref <<EOF
|
||||
# The GTX 1070 is Pascal. Branch ${DRIVER_BRANCH} is the last one that supports it.
|
||||
# Anything newer will install cleanly and then fail to drive the card.
|
||||
Package: nvidia-driver-6* nvidia-driver-59*
|
||||
Pin: release *
|
||||
Pin-Priority: -1
|
||||
EOF
|
||||
|
||||
echo " pinned against post-${DRIVER_BRANCH} branches (Pascal EOL guard)"
|
||||
exit 0
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 40-shell-mcp — build and start the NATIVE shell-mcp on this host.
|
||||
#
|
||||
# This replaces the mediabox-mcp SSH proxy that currently runs on arrsstack.
|
||||
# Once this is up and NPM proxy host 42 is repointed to 10.0.1.20:8103, the
|
||||
# proxy container and its SSH key get deleted. The public URL, the LE cert and
|
||||
# the Claude connector entry do not change.
|
||||
#
|
||||
# BUILDS LOCALLY. It does not pull an image. The arrsstack image is arm64 and
|
||||
# would not run here; and there is no registry in this homelab to pull from.
|
||||
# amd64 buildability was verified before this was written — the full Python
|
||||
# dependency tree resolves to prebuilt manylinux x86_64 wheels, so no compiler
|
||||
# is required and the build takes about a minute.
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SRC="$REPO_DIR/shell-mcp"
|
||||
DEST="/srv/shell-mcp"
|
||||
ENVFILE="/srv/secrets/stacks/shell-mcp.env"
|
||||
MASTER="/srv/secrets/stacks.env"
|
||||
|
||||
command -v docker >/dev/null 2>&1 || { echo " [FAIL] docker not installed"; exit 1; }
|
||||
|
||||
# --- resolve the bearer token ------------------------------------------------
|
||||
# Reuses the SAME token arrsstack's mediabox-mcp serves today, so the Claude
|
||||
# connector entry survives the cutover untouched.
|
||||
TOKEN="$(grep -E '^MEDIABOX_MCP_BEARER_TOKEN=' "$MASTER" 2>/dev/null | head -1 | cut -d= -f2-)"
|
||||
if [ -z "${TOKEN:-}" ] || [ "$TOKEN" = "FILL_ME" ]; then
|
||||
cat <<'MSG'
|
||||
[SKIP] MEDIABOX_MCP_BEARER_TOKEN is not set in /srv/secrets/stacks.env.
|
||||
|
||||
Copy the existing value from arrsstack so the connector keeps working:
|
||||
# on arrsstack
|
||||
grep '^MEDIABOX_MCP_BEARER_TOKEN=' /srv/secrets/stacks.env
|
||||
then put it in this host's /srv/secrets/stacks.env and re-run:
|
||||
sudo /srv/mediabox-bootstrap/scripts/40-shell-mcp.sh
|
||||
MSG
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- stage source ------------------------------------------------------------
|
||||
install -d -m 0755 "$DEST"
|
||||
install -m 0644 "$SRC/server.py" "$DEST/server.py"
|
||||
install -m 0644 "$SRC/requirements.txt" "$DEST/requirements.txt"
|
||||
install -m 0644 "$SRC/Dockerfile" "$DEST/Dockerfile"
|
||||
install -m 0644 "$SRC/docker-compose.yml" "$DEST/docker-compose.yml"
|
||||
|
||||
# --- per-stack env slice (same convention as arrsstack) ----------------------
|
||||
install -d -m 0700 /srv/secrets/stacks
|
||||
umask 077
|
||||
{
|
||||
printf 'MEDIABOX_MCP_BEARER_TOKEN=%s\n' "$TOKEN"
|
||||
printf 'TZ=%s\n' "$(grep -E '^TZ=' "$MASTER" | head -1 | cut -d= -f2- || echo America/New_York)"
|
||||
} > "$ENVFILE"
|
||||
chmod 600 "$ENVFILE"
|
||||
|
||||
# --- build & start -----------------------------------------------------------
|
||||
echo " building shell-mcp for amd64 (local build, no pull)"
|
||||
cd "$DEST" || exit 1
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "$ENVFILE"
|
||||
set +a
|
||||
|
||||
if ! docker compose up -d --build; then
|
||||
echo " [FAIL] build/start failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- verify ------------------------------------------------------------------
|
||||
echo " waiting for health endpoint"
|
||||
for i in $(seq 1 30); do
|
||||
if curl -fsS -m 3 "http://127.0.0.1:8103/health" >/dev/null 2>&1; then
|
||||
echo " [ok] /health responding: $(curl -fsS -m 3 http://127.0.0.1:8103/health)"
|
||||
|
||||
# An unauthenticated /sse MUST be rejected. If this ever returns 200 the
|
||||
# box is publicly shell-able through NPM.
|
||||
code="$(curl -s -o /dev/null -w '%{http_code}' -m 5 "http://127.0.0.1:8103/sse" || true)"
|
||||
if [ "$code" = "401" ]; then
|
||||
echo " [ok] /sse rejects unauthenticated requests (401)"
|
||||
else
|
||||
echo " [FAIL] /sse returned $code without a token — DO NOT repoint NPM until fixed"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo " [FAIL] health endpoint never came up"
|
||||
docker compose logs --tail 40 shell-mcp 2>&1 | sed 's/^/ /'
|
||||
exit 1
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# 50-plex — deploy Plex. HUMAN-IN-THE-LOOP, run by hand, not from bootstrap.
|
||||
#
|
||||
# Usage:
|
||||
# # 1. open https://plex.tv/claim and copy the token
|
||||
# # 2. within four minutes:
|
||||
# sudo PLEX_CLAIM=claim-xxxxxxxxxxxx /srv/mediabox-bootstrap/scripts/50-plex.sh
|
||||
#
|
||||
# A claim token expires four minutes after it is issued. That is the entire
|
||||
# reason this stage is not automated: there is no way to bake one into a USB,
|
||||
# a repo, or a first-boot script and have it still be valid.
|
||||
#
|
||||
# Claiming is only required on the FIRST start. Re-running later without
|
||||
# PLEX_CLAIM is fine and will not re-claim.
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DEST="/srv/plex"
|
||||
MASTER="/srv/secrets/stacks.env"
|
||||
|
||||
# --- gate 1: mounts must be live BEFORE Plex ever scans ----------------------
|
||||
# If Plex scans a library whose mount is missing, it sees zero files. With a
|
||||
# read-write NAS account, "empty trash after scan" would then delete the
|
||||
# library's records — and Plex has the permission to act on that. Never let
|
||||
# Plex start against absent mounts.
|
||||
echo " checking NAS mounts before starting Plex"
|
||||
missing=0
|
||||
for mp in /mnt/nas/audiobooks /mnt/nas/education-videos /mnt/nas/health \
|
||||
/mnt/nas/home-movies /mnt/nas/media /mnt/nas/music-organized \
|
||||
/mnt/nas/pictures /mnt/nas/radio-shows; do
|
||||
if ls "$mp" >/dev/null 2>&1 && mountpoint -q "$mp"; then
|
||||
printf ' [ok] %s\n' "$mp"
|
||||
else
|
||||
printf ' [FAIL] %s not mounted\n' "$mp"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
if [ "$missing" -ne 0 ]; then
|
||||
echo
|
||||
echo " REFUSING to start Plex with missing mounts."
|
||||
echo " Fix with: sudo /srv/mediabox-bootstrap/scripts/20-cifs.sh --verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- gate 2: Quick Sync -----------------------------------------------------
|
||||
if [ ! -e /dev/dri/renderD128 ]; then
|
||||
echo " [WARN] /dev/dri/renderD128 missing — iGPU not enabled in BIOS."
|
||||
echo " Plex will still run, but every transcode will be software."
|
||||
RENDER_GID=993
|
||||
else
|
||||
RENDER_GID="$(stat -c '%g' /dev/dri/renderD128)"
|
||||
echo " render node gid: $RENDER_GID"
|
||||
fi
|
||||
export RENDER_GID
|
||||
|
||||
install -d -m 0755 "$DEST"
|
||||
install -d -m 0755 "$DEST/config"
|
||||
chown -R 3000:3000 "$DEST"
|
||||
install -m 0644 "$REPO_DIR/plex/docker-compose.yml" "$DEST/docker-compose.yml"
|
||||
|
||||
# --- env --------------------------------------------------------------------
|
||||
set -a
|
||||
TZ="$(grep -E '^TZ=' "$MASTER" 2>/dev/null | head -1 | cut -d= -f2- || echo America/New_York)"
|
||||
PLEX_ADVERTISE_URL="$(grep -E '^PLEX_ADVERTISE_URL=' "$MASTER" 2>/dev/null | head -1 | cut -d= -f2- || echo http://10.0.1.20:32400)"
|
||||
PLEX_CLAIM="${PLEX_CLAIM:-}"
|
||||
set +a
|
||||
|
||||
if [ -n "$PLEX_CLAIM" ]; then
|
||||
echo " claim token supplied (expires 4 min from issue — moving now)"
|
||||
else
|
||||
echo " no PLEX_CLAIM given; assuming this server is already claimed"
|
||||
fi
|
||||
|
||||
cd "$DEST" || exit 1
|
||||
docker compose up -d || { echo " [FAIL] compose up failed"; exit 1; }
|
||||
|
||||
# The claim token must not linger anywhere on disk.
|
||||
unset PLEX_CLAIM
|
||||
|
||||
echo " waiting for Plex to answer on 32400"
|
||||
for i in $(seq 1 60); do
|
||||
if curl -fsS -m 3 "http://127.0.0.1:32400/identity" >/dev/null 2>&1; then
|
||||
echo " [ok] Plex is up"
|
||||
break
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
|
||||
# --- gate 3: disable auto-empty-trash, permanently ---------------------------
|
||||
# This is the single most dangerous default on this box. If a CIFS mount is
|
||||
# missing at scan time Plex sees an empty library; with autoEmptyTrash on it
|
||||
# begins trimming, and the NAS account is read-write. Turn it off in the
|
||||
# config rather than trusting a UI checkbox to stay ticked.
|
||||
PREFS="$DEST/config/Library/Application Support/Plex Media Server/Preferences.xml"
|
||||
if [ -f "$PREFS" ]; then
|
||||
if grep -q 'autoEmptyTrash="0"' "$PREFS"; then
|
||||
echo " [ok] autoEmptyTrash already disabled"
|
||||
else
|
||||
echo " disabling autoEmptyTrash (requires a Plex restart)"
|
||||
docker compose stop plex >/dev/null 2>&1
|
||||
cp -a "$PREFS" "${PREFS}.bak.$(date +%Y%m%d%H%M%S)"
|
||||
if grep -q 'autoEmptyTrash=' "$PREFS"; then
|
||||
sed -i 's/autoEmptyTrash="[^"]*"/autoEmptyTrash="0"/' "$PREFS"
|
||||
else
|
||||
sed -i 's/<Preferences /<Preferences autoEmptyTrash="0" /' "$PREFS"
|
||||
fi
|
||||
chown 3000:3000 "$PREFS"
|
||||
docker compose start plex >/dev/null 2>&1
|
||||
echo " [ok] autoEmptyTrash=0 written"
|
||||
fi
|
||||
else
|
||||
cat <<'MSG'
|
||||
[WARN] Preferences.xml not written yet (first start is still initialising).
|
||||
Re-run this script once Plex has fully started to disable
|
||||
autoEmptyTrash, OR untick it by hand:
|
||||
Settings > Library > "Empty trash automatically after every scan"
|
||||
Do this BEFORE adding any library. With read-write NAS credentials a
|
||||
missing mount plus this setting deletes media records.
|
||||
MSG
|
||||
fi
|
||||
|
||||
cat <<'NEXT'
|
||||
|
||||
Next:
|
||||
* Open http://10.0.1.20:32400/web and confirm the server is claimed.
|
||||
* Add libraries pointing at /media/... (the container paths), not /mnt/nas.
|
||||
* Verify a transcode is using Quick Sync:
|
||||
docker exec plex ls -l /dev/dri
|
||||
# start a transcode, then:
|
||||
intel_gpu_top # Video/VideoEnhance rows should be busy
|
||||
NEXT
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user