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:
Claude
2026-07-27 18:12:44 -04:00
co-authored by Claude Opus 5
parent 965f5f5ba0
commit 7eadcdf9e8
14 changed files with 1142 additions and 4 deletions
+156
View File
@@ -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