Bring script headers onto the template and close safeguard gaps

Headers claimed protections the code never had, and several destructive paths had no
guard against a collapsed config value.
This commit is contained in:
Gmer4Lfe
2026-08-01 20:37:59 -04:00
parent cdce877601
commit e8b114094a
78 changed files with 3301 additions and 277 deletions
+50
View File
@@ -18,6 +18,41 @@
# do not need to be hardcoded and work across hosts.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Sonarr then Radarr, each independently:
#
# 1. Fetch quality profiles and resolve the configured names to live IDs
# 2. Fetch the library
# 3. Per item, derive the expected profile from its rootFolderPath
# contains "kids" or "anime" → kids profile
# anything else → default profile
# 4. Compare against the item's current profile
# already correct → skip, no call made
# wrong → queue for the bulk editor PUT
# 5. Issue the change and report counts
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Resolve Profile IDs at Runtime
# Profiles are looked up by name from the API on every run rather than hardcoding IDs.
# Profile IDs differ between hosts and change when profiles are recreated; a stale
# hardcoded ID would silently assign the wrong profile rather than failing.
#
# Change Only What Is Wrong
# Items already on the correct profile are never touched. That makes the tool safe to
# re-run at any time and keeps the reported count meaningful — a non-zero result means
# real drift, not just "it ran".
#
# Root Folder Is the Source of Truth
# Classification comes from where the item actually lives, not from its metadata. The
# classification scans own the harder question of whether an item is in the right root;
# this tool just makes the profile agree with the answer already on disk.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
@@ -69,6 +104,21 @@ done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
# Consistent with the rest of Arrs_Stack/ and Tools/ — this issues bulk profile PUTs against
# the arrs and should not be runnable by an unprivileged account that happens to hold a key.
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
# Bulk PUTs against the same items — two concurrent runs could interleave and leave a subset
# of series/movies on the wrong profile.
acquire_lock
detect_hosts
KIDS_PROFILE_NAME="${ARR_KIDS_PROFILE_NAME:-Kids shows}"
+56
View File
@@ -19,6 +19,58 @@
# this for scans it triggers itself; this tool covers everything else.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Validate the arr type and resolve its URL / API key / API version
# 2. Poll /command for any active rescan-type command (ARR_RESCAN_COMMANDS)
# none active → nothing to watch, exit cleanly
# 3. Wait for it to leave the active state, bounded by the timeout argument
# (default 7200s — a full-library scan is slow by nature)
# 4. Once finished, fetch the library and arr_cache_write() the real numbers
#
# Triggers nothing. It only watches a scan someone or something else started.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Watch, Never Trigger
# This tool deliberately starts nothing. Its whole job is to be the thing that notices a
# scan finished, so a rescan begun by hand, by the arr's own UI, or by another script
# still gets its result written through to the shared cache.
#
# Complement to the Write Guard
# arr_cache_write() refuses to write while a rescan is active, because a mid-scan snapshot
# reads as real data loss to consumers like check_tracked_count_floor. That guard protects
# the cache but leaves nobody to write the true number afterwards. arr_full_rescan.sh
# covers the scans it starts itself; this covers every other origin.
#
# Bounded Wait
# The wait is capped by the timeout argument, so a scan that stalls or never reports
# completion cannot leave this running indefinitely holding its lock.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf (aliased by detect_hosts())
#
# SONARR_URL / SONARR_API_KEY
# RADARR_URL / RADARR_API_KEY
# LIDARR_URL / LIDARR_API_KEY
# Connection details for whichever arr is passed as the first argument.
#
# common.sh
#
# ARR_RESCAN_COMMANDS
# Command names treated as a rescan for this purpose — shared with
# arr_cache_write()'s own active-scan guard so both agree on what counts.
#
# ARR_API_VERSION
# Per-arr API version map (v3 for Sonarr/Radarr, v1 for Lidarr).
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
@@ -59,6 +111,10 @@ if [[ "$EUID" -ne 0 ]]; then
exit 1
fi
# This writes the shared tracked-data cache on completion. Two monitors watching the same
# arr would both write it, and the loser's stale snapshot could land last.
acquire_lock
detect_hosts
ARR_TYPE="${PARSED_ARGS[0]:-}"
+35 -3
View File
@@ -23,9 +23,32 @@
# repairs this tool is meant for — worth remembering before pointing it at an entire share.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# For each path given on the command line:
#
# 1. Path guard — refuse anything shallower than two components
# 2. Existence check — a missing path is a failure for that entry, not the run
# 3. Report scale — file count, dir count, total size, so the operator sees the job size
# 4. Count entries with wrong ownership (the diagnostic number in the summary)
# 5. chown -R PERMISSIONS_OWNER across the path
# 6. find -type d → chmod PERMISSIONS_DIR_MODE
# 7. find -type f → chmod PERMISSIONS_FILE_MODE
#
# Unlike the nightly media_shares_permissions.sh, steps 57 are unconditional — see the
# ctime note in PURPOSE above for why that distinction matters.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Unconditional by Design, Unlike the Nightly Job
# media_shares_permissions.sh applies its passes conditionally to protect ctime as an
# age signal for arr orphan collection. This tool deliberately does not: it exists to
# repair paths that are known-wrong, where correctness matters more than preserving a
# clock. That is exactly why it is a targeted manual tool and not a scheduled one.
#
# Permissions Model
# Directories (PERMISSIONS_DIR_MODE, default 755):
# Owner (nobody) — rwx enter, list, create files
@@ -53,9 +76,6 @@
# Each path is verified before processing — missing paths log an error and
# are skipped rather than silently passing.
#
# Notification Validated
# platform_require_cmd confirms the notify script is present before use.
#
# Silent on Success
# Only failures and the wrong-owner diagnostic produce visible output.
#
@@ -137,6 +157,18 @@ for share_path in "${PARSED_ARGS[@]}"; do
echo ""
echo "━━━ $ICON_PERMS $(basename "$share_path") ━━━"
# chown -R below. Paths come straight from the command line, so a spacing typo
# ("/mnt/user /Movies" instead of "/mnt/user/Movies") would hand this a bare top-level
# directory — and chown -R nobody:users on / or /etc breaks the system outright.
# Require at least two path components; that still allows a deliberate whole-share
# repair like /mnt/user while refusing /, /mnt, /etc, /boot and friends.
_bpr_slashes="${share_path//[^\/]/}"
if [[ "$share_path" != /* || "${#_bpr_slashes}" -lt 2 ]]; then
error "$share_path — refusing: expected an absolute path at least 2 levels deep"
FAIL+=("$share_path")
continue
fi
if [[ ! -d "$share_path" ]]; then
error "$share_path — not found"
FAIL+=("$share_path")
+31 -3
View File
@@ -13,6 +13,37 @@
# Output: ContainerName_YYYY-MM-DD_HH-MM.tar.gz — timestamped, no overwrite.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Validate arguments — container name, appdata path, output directory all required
# 2. Verify the output directory exists and holds enough free space for the archive
# 3. Record whether the container is currently running
# 4. Stop the container if it was running
# 5. tar czf the appdata directory to a timestamped archive
# 6. Verify the archive with tar --test-file
# 7. Restart the container only if it was running before — a container found stopped
# stays stopped
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# This tool takes everything as arguments rather than from conf:
#
# <container> Container to stop for the duration of the export
# <appdata_path> Directory to archive
# <output_dir> Destination for the archive — must already exist
#
# That is deliberate. It is used for one-off exports of arbitrary containers, including
# ones being removed from the stack entirely, so there is no meaningful configured list
# to draw from and nothing host-specific to alias.
#
# Note the archive is written as root and is not chowned afterwards. That is fine for the
# operator-invoked use this tool is for, but worth knowing if the output directory is a
# user share reached over SMB.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
@@ -46,9 +77,6 @@
# DOCKER_TIMEOUT (default: 30s) caps all docker calls. Guards against a hung
# daemon blocking the script indefinitely.
#
# Notification Validated
# platform_require_cmd confirms the notify script is present before use.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
+51 -4
View File
@@ -30,13 +30,60 @@
# that may be intentionally paused.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Default (dangling only):
# docker image prune -f — removes untagged, unreferenced images only. Every tagged
# image survives, running containers are untouched, and nothing stopped is removed.
#
# --all (full orphan cleanup):
# 1. Remove containers in exited/created state
# 2. Remove every image not used by a RUNNING container
# Step 1 is what makes step 2 reach further: with the stopped containers gone, their
# images are no longer referenced and become eligible. That is also precisely why
# --all is destructive to anything deliberately kept stopped.
#
# Reclaimed space is reported for both modes.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# This tool is deliberately unconfigured — it reads live Docker state rather than any
# configured list, so there is nothing host-specific to alias and no thresholds to tune.
# Scope is controlled entirely by the mode flag (default vs --all).
#
# Note it does not call detect_hosts(): nothing here is host-specific, and it acts only on
# the local daemon.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root check — Docker prune operations require root
# acquire_lock prevents concurrent prune runs
# --dry-run — shows what would be removed without taking any action
# --status — lists current dangling images and stopped containers; no changes
# Root Enforcement
# Docker prune operations require root.
#
# Lock Acquisition
# acquire_lock prevents concurrent prune runs racing on the same image store.
#
# Dangling-Only Default
# The default mode removes untagged, unreferenced images only. Reaching anything
# tagged, running, or deliberately stopped requires --all explicitly — the safe
# behaviour is what you get by not thinking about it.
#
# Running Containers Never Touched
# Neither mode removes a running container or an image a running container uses.
# --all widens the blast radius to STOPPED containers and their images, never to
# anything currently up.
#
# Dry Run Support
# --dry-run lists everything that would be removed and removes nothing. Worth using
# before --all specifically, since that mode deletes intentionally-stopped containers.
#
# Status Mode
# --status lists current dangling images and stopped containers without changing
# anything, so the scope of a prospective --all is visible up front.
#
# ==============================================================================================
# RUNTIME MODES
+22
View File
@@ -29,6 +29,28 @@
# Emby's config path is detected from the Docker mount — no hardcoded paths.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Diagnose, Never Repair
# The script reports which databases fail integrity_check and stops there. SQLite
# "repair" means dumping and rebuilding, which silently discards whatever rows were
# corrupt — an outcome nobody should get without deciding to. The remediation steps
# are printed instead, for a human to run against a backup.
#
# Stop Emby Before Reading
# integrity_check against a live database gives unreliable answers and can itself
# contend with Emby's writes. Emby is stopped for the duration and restarted after,
# so the check runs against a quiescent file.
#
# Always Restart, Even on Failure
# Emby is brought back up regardless of what the check found, and an EXIT trap armed
# before it is stopped restores it even if this script dies partway through. It is
# disarmed only once the normal restart has run. Leaving the media server down because
# a diagnostic reported a problem turns an investigation into an outage — and the
# original running state is honoured, so an Emby that was already stopped stays stopped.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
+25 -2
View File
@@ -17,7 +17,7 @@
# fetch as fallback.
#
# ==============================================================================================
# FLOW
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Pull play completions from Emby activity log (all time, or --days N)
@@ -25,6 +25,29 @@
# 3. For each played artist not in Lidarr → add to Lidarr
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Add-Only, Never Remove
# The tool only adds artists the arr is missing. It never deletes or unmonitors anything,
# so the worst outcome of a bad match is an extra tracked entry — trivially reversible —
# rather than lost tracking on something already curated.
#
# Library Membership Is the Whole Criterion
# No scoring, no thresholds, no metadata quality gates. If it is in Emby, the arr should
# know about it. This is deliberately not a discovery tool; the playback_aware_* scripts
# own that job and its judgement calls.
#
# Provider ID Over Title
# Matching prefers the MusicBrainz ID and falls back to case-insensitive title only when the ID
# is absent. Titles differ across sources by punctuation, year suffixes and articles;
# matching on them alone would re-add things the arr already tracks.
#
# Cache-First Read
# The arr library comes from the shared tracked-data cache, kept warm by
# arr_cache_prefill.sh, with a live fetch as fallback. One read regardless of library size.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
@@ -40,7 +63,7 @@
# modification. Safe to run multiple times — the second run finds nothing to add.
#
# ==============================================================================================
# CONFIGURATION (host*.conf)
# CONFIGURATION
# ==============================================================================================
#
# HOST*_LIDARR_URL / HOST*_LIDARR_API_KEY — Lidarr connection (aliased by detect_hosts)
+25 -2
View File
@@ -16,7 +16,7 @@
# fetch as fallback.
#
# ==============================================================================================
# FLOW
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Fetch all Movie items from Emby (with TMDB provider IDs)
@@ -27,6 +27,29 @@
# falling back to case-insensitive title comparison.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Add-Only, Never Remove
# The tool only adds movies the arr is missing. It never deletes or unmonitors anything,
# so the worst outcome of a bad match is an extra tracked entry — trivially reversible —
# rather than lost tracking on something already curated.
#
# Library Membership Is the Whole Criterion
# No scoring, no thresholds, no metadata quality gates. If it is in Emby, the arr should
# know about it. This is deliberately not a discovery tool; the playback_aware_* scripts
# own that job and its judgement calls.
#
# Provider ID Over Title
# Matching prefers the TMDB ID and falls back to case-insensitive title only when the ID
# is absent. Titles differ across sources by punctuation, year suffixes and articles;
# matching on them alone would re-add things the arr already tracks.
#
# Cache-First Read
# The arr library comes from the shared tracked-data cache, kept warm by
# arr_cache_prefill.sh, with a live fetch as fallback. One read regardless of library size.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
@@ -42,7 +65,7 @@
# without modification. Safe to run multiple times — the second run finds nothing to add.
#
# ==============================================================================================
# CONFIGURATION (master.conf / host*.conf)
# CONFIGURATION
# ==============================================================================================
#
# RADARR_EMBY_LIBRARIES — Emby library names to scan (master.conf); empty = all libraries
+25 -2
View File
@@ -16,7 +16,7 @@
# fetch as fallback.
#
# ==============================================================================================
# FLOW
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Fetch all Series items from Emby (with TVDB provider IDs)
@@ -27,6 +27,29 @@
# falling back to case-insensitive title comparison.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Add-Only, Never Remove
# The tool only adds series the arr is missing. It never deletes or unmonitors anything,
# so the worst outcome of a bad match is an extra tracked entry — trivially reversible —
# rather than lost tracking on something already curated.
#
# Library Membership Is the Whole Criterion
# No scoring, no thresholds, no metadata quality gates. If it is in Emby, the arr should
# know about it. This is deliberately not a discovery tool; the playback_aware_* scripts
# own that job and its judgement calls.
#
# Provider ID Over Title
# Matching prefers the TVDB ID and falls back to case-insensitive title only when the ID
# is absent. Titles differ across sources by punctuation, year suffixes and articles;
# matching on them alone would re-add things the arr already tracks.
#
# Cache-First Read
# The arr library comes from the shared tracked-data cache, kept warm by
# arr_cache_prefill.sh, with a live fetch as fallback. One read regardless of library size.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
@@ -42,7 +65,7 @@
# without modification. Safe to run multiple times — the second run finds nothing to add.
#
# ==============================================================================================
# CONFIGURATION (master.conf / host*.conf)
# CONFIGURATION
# ==============================================================================================
#
# SONARR_EMBY_LIBRARIES — Emby library names to scan (master.conf); empty = all libraries
+43 -3
View File
@@ -22,6 +22,49 @@
# covering the remote server until the next detection cycle.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Read and display the current state file so the operator sees what is being discarded
# 2. Write a fresh state file:
# state=NORMAL, fallback_start=0, handback_strikes=0
# tier2_started=false, tier3_started=false, tier4_started=false
# 3. Report the new state
#
# Nothing else is touched. No container is started or stopped, no DDNS is moved, no rsync
# is run. fallback.sh picks the new state up on its next cycle and proceeds from NORMAL.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# State File Only, Never Containers
# This tool changes what fallback.sh believes, not what is actually running. That
# separation is the point: reconciling the real stack is an operator judgement call,
# and a tool that tried to do both could act on a belief that was already wrong.
#
# The Operator Asserts Reality
# Resetting to NORMAL is a claim that the stack really is normal — right containers on
# the right host, DDNS pointing the right way. The script cannot verify that, so it
# shows the current state before overwriting it and leaves the check to the human. If
# the assertion is wrong, fallback.sh will act on a false NORMAL.
#
# Full Reset, Not Partial Edit
# Every field is rewritten rather than patching individual keys. A partially-reset file
# — NORMAL state with tier flags still true — is a state fallback.sh has no handling
# for and would be worse than either extreme.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# FALLBACK_STATE_FILE
# Path to the fallback state file this tool rewrites. Shared with fallback.sh —
# both must agree or the reset writes somewhere fallback.sh never reads.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
@@ -40,9 +83,6 @@
# Interactive mode prompts for YES before writing. Use --force to bypass in
# non-interactive contexts (cron, scripts).
#
# Notification Validated
# platform_require_cmd confirms the notify script is present before use.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
+25
View File
@@ -30,6 +30,31 @@
# lost on unmount — warn the user but proceed (this is expected for maintenance).
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Redirect Before Unmounting
# The symlink is pointed at the SSD fallback first, and only then is the tmpfs
# unmounted. Reversing that order would leave in-flight transcodes writing into a
# path that is being pulled out from under them. New sessions land on SSD from the
# moment of the flip; existing ones drain.
#
# Unmount Is the Data Loss
# Everything on a tmpfs disappears when it is unmounted — there is nothing to migrate
# and no way to preserve it. That is acceptable only because the contents are
# regenerable transcode segments, which is exactly why this operation is safe for
# transcodes and would not be for any other kind of ramdisk.
#
# Not Mounted Is Success
# An already-unmounted ramdisk is reported and treated as done, not as an error. The
# goal state is "ramdisk not mounted", and the script is idempotent toward it.
#
# State File Reflects Reality
# The transcode state file is updated to record the SSD target, so transcode_manager.sh
# agrees with what actually happened rather than trying to flip back to a ramdisk that
# no longer exists.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
+23
View File
@@ -31,6 +31,29 @@
# NVMe any : 510 min
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Read-Only Diagnostic
# A SMART self-test is executed by the drive's own firmware and writes nothing to the
# filesystem. The array stays fully online and in use throughout; the only cost is
# background I/O contention while the drive read-scans itself.
#
# Full Scan, Not the Short Test
# The extended test reads every sector. The short test samples, and a bad sector in
# the unsampled region is exactly the kind of latent fault that only surfaces when
# something tries to read it — often during a rebuild, when redundancy is already gone.
#
# Per-Drive Isolation
# A drive that fails to start a test, does not support one, or reports an error never
# prevents the remaining drives from being tested. Partial coverage beats none.
#
# Report, Do Not Act
# Results are reported and notified; nothing is disabled, replaced or rebuilt on the
# basis of a failed test. Acting on a drive is a hardware decision with cost and
# downtime attached, and belongs to the operator.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
+70
View File
@@ -41,6 +41,64 @@
# safely after a partial run or a manually resolved conflict.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Move, Never Copy or Delete
# Files are relocated with mv and nothing is ever removed. A misjudged move is
# reversible by hand; a delete is not. The script has no cleanup pass by design.
#
# Idempotent by Collision Skip
# Re-running is safe because an existing destination is skipped rather than
# overwritten. A partial run, an interrupted run, or a manually resolved conflict
# can all be followed by a plain re-run.
#
# Deterministic Ordering
# Multiple trailers per show are sorted before numbering, so the same input always
# produces the same trailer.ext / trailer-2.ext assignment. Without the sort, the
# numbering would depend on filesystem iteration order and a re-run could rename
# files differently.
#
# Filesystem Only
# Neither Trailarr nor Emby is touched. Emby's periodic library scan picks the moved
# files up on its own — poking either service would add failure modes to what is
# otherwise a pure file move.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
# Moves files owned by container users and chowns the created trailers/ directory.
#
# Lock Acquisition
# acquire_lock prevents two runs numbering the same show's trailers concurrently.
#
# Host Detection
# detect_hosts() aliases HOST*_SONARR_TV_ROOT → SONARR_TV_ROOT.
#
# TV Root Validation
# Exits if SONARR_TV_ROOT is unset or is not a directory — the scan below is rooted
# there, and an empty value would walk from the current directory.
#
# Bounded Scan Depth
# find runs with -mindepth 2 -maxdepth 2, so only files sitting directly in a series
# folder are considered. Trailers already correctly placed inside trailers/ are out
# of range and cannot be picked up and re-moved.
#
# Collision Skip
# An existing destination is never overwritten — the source is left in place and
# counted as a conflict for review.
#
# Permission Matching on Created Directories
# mkdir as root would leave trailers/ as root-owned. It is chowned to
# PERMISSIONS_OWNER and chmodded to PERMISSIONS_DIR_MODE so it matches the rest of
# the library and does not become an exception the permissions job has to correct.
#
# Dry Run Support
# --dry-run reports every move and touches nothing.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
@@ -50,6 +108,12 @@
# Host filesystem path to the TV library. Aliased by detect_hosts() →
# SONARR_TV_ROOT.
#
# master.conf
#
# PERMISSIONS_OWNER / PERMISSIONS_DIR_MODE
# Applied to each created trailers/ directory so it matches library convention.
# Shared with media_shares_permissions.sh. (defaults: nobody:users, 755)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
@@ -74,6 +138,12 @@ parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
# Moves container-owned media files and chowns the trailers/ dirs it creates.
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
acquire_lock
# detect_hosts() sets MY_ID and aliases HOST*_SONARR_TV_ROOT → SONARR_TV_ROOT
+39
View File
@@ -28,6 +28,45 @@
# 4. Watchdog monitors normally on next cycle. If it crashes again → re-added.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Clearing Is an Assertion, Not a Fix
# Removing a container from the skip list tells docker_watchdog.sh to start restarting
# it again. It does nothing about why the container was crash-looping. Clearing before
# the underlying fault is fixed just restarts the loop that put it there.
#
# The Skip List Is Protection, Not Punishment
# A container lands here because restarting it repeatedly was making things worse, not
# better. The entry exists so the watchdog stops burning cycles and notifications on
# something only a human can fix.
#
# Persistent by Design
# The list lives on /boot/config and survives reboots deliberately. A crash loop that
# a reboot would clear is exactly the case where the watchdog would resume looping
# after the reboot — persistence is what stops that.
#
# Auto-Clear Is the Normal Path
# docker_watchdog.sh removes a container from the list on its own once it sees it
# running healthily. This tool is for the case where you have fixed the problem and
# do not want to wait for that, not the routine way entries leave the list.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# DOCKER_WATCHDOG_FAILED_FILE
# Persistent skip list on /boot/config, shared with docker_watchdog.sh. Both must
# agree on the path or the watchdog will not see what this tool changes.
#
# WATCHDOG_CONTAINER_RESTART_LIMIT / WATCHDOG_CONTAINER_RESTART_WINDOW
# The thresholds docker_watchdog.sh uses to decide a container belongs on the list.
# Shown here for context — this tool does not apply them, it only views and edits
# the resulting list.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
+57 -2
View File
@@ -43,6 +43,34 @@
# acquire_lock — prevents concurrent registration runs
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# master.conf
#
# WEBHOOK_PORT
# Port the listener binds and the registered webhook URL points at. 0 disables the
# listener entirely (start_webhook_listener.sh exits early), so registering against
# a port of 0 would produce URLs nothing is serving.
#
# WEBHOOK_SECRET
# Shared secret embedded in the registered URL as ?key=. Generated here on first run
# and written back into master.conf — the write-back is verified, because the arrs
# are registered with this value and a failed persist would leave them holding a
# secret this host does not have.
#
# host*.conf (aliased by detect_hosts())
#
# SONARR_URL / SONARR_API_KEY
# RADARR_URL / RADARR_API_KEY
# LIDARR_URL / LIDARR_API_KEY
# Each arr the webhook is registered in. An arr with no URL or key configured on
# this host is skipped rather than failing the run.
#
# SSH_KEY
# Used when propagating the same secret to the partner via OVERRIDE_SECRET.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
@@ -76,6 +104,13 @@ MASTER_CONF="$ECOSYSTEM_ROOT/Configurations/master.conf"
source "$ECOSYSTEM_ROOT/load_config.sh"
parse_args "$@"
# Writes the generated secret into master.conf via sed -i and SSHes to the partner.
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
acquire_lock
detect_hosts
@@ -84,9 +119,25 @@ WEBHOOK_NAME="Varaverk Upgrade"
# ── Resolve or generate the secret ──────────────────────────────────────────
# OVERRIDE_SECRET env var is set when called recursively via SSH from the
# primary host, so both ends use the same secret.
#
# Every write-back below is verified by re-reading master.conf. The secret gets baked into
# the webhook URL registered in each arr — if the sed silently fails to match, the arrs end
# up holding a secret this host does not have, and the listener rejects every delivery.
_persist_secret() {
local secret="$1"
sed -i "s/WEBHOOK_SECRET=\"\"/WEBHOOK_SECRET=\"$secret\"/" "$MASTER_CONF"
if ! grep -q "WEBHOOK_SECRET=\"$secret\"" "$MASTER_CONF" 2>/dev/null; then
error "Could not persist WEBHOOK_SECRET to $MASTER_CONF"
error "Registering the arrs now would leave them with a secret this host does not have"
notify "Webhook setup aborted on $(hostname) — could not persist WEBHOOK_SECRET" \
"Webhook Setup" "warning"
exit 1
fi
}
if [[ -n "${OVERRIDE_SECRET:-}" ]]; then
if [[ -z "${WEBHOOK_SECRET:-}" ]]; then
sed -i "s/WEBHOOK_SECRET=\"\"/WEBHOOK_SECRET=\"$OVERRIDE_SECRET\"/" "$MASTER_CONF"
_persist_secret "$OVERRIDE_SECRET"
fi
WEBHOOK_SECRET="$OVERRIDE_SECRET"
fi
@@ -95,8 +146,12 @@ if [[ -z "${WEBHOOK_SECRET:-}" ]]; then
if [[ "$DRY_RUN" == true ]]; then
WEBHOOK_SECRET="<would-generate>"
else
if ! command -v openssl >/dev/null 2>&1; then
error "openssl not found — cannot generate WEBHOOK_SECRET"
exit 1
fi
GENERATED=$(openssl rand -hex 32)
sed -i "s/WEBHOOK_SECRET=\"\"/WEBHOOK_SECRET=\"$GENERATED\"/" "$MASTER_CONF"
_persist_secret "$GENERATED"
WEBHOOK_SECRET="$GENERATED"
echo "Generated WEBHOOK_SECRET — saved to master.conf"
fi
+27
View File
@@ -27,6 +27,33 @@
# VM pools, temp pools, etc.). Specifying a pool by name bypasses the ignore list.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Scrub While Online
# A scrub runs against a live, in-use pool by design. Nothing is unmounted and no
# service is stopped — the cost is background I/O, not downtime, which is what makes
# monthly cadence practical at all.
#
# Detect Before Redundancy Is Gone
# Silent corruption otherwise surfaces only when the bad block is finally read, which
# is often during a resilver — precisely when the redundancy needed to repair it is
# already spent. Scrubbing is what moves that discovery to a moment when ZFS can still
# fix it from a good copy.
#
# Never Start a Second Scrub
# A pool already scrubbing is left alone rather than restarted. Restarting discards the
# progress of the run in flight and begins the whole read again.
#
# Per-Pool Isolation
# One pool failing, being unavailable, or already scrubbing never blocks the others.
#
# Report Errors, Repair Nothing by Hand
# ZFS self-heals from redundancy during the scrub. What this reports is what ZFS could
# not fix — those are operator decisions about hardware, not something a script should
# attempt to resolve.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#