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:
@@ -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}"
|
||||
|
||||
@@ -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]:-}"
|
||||
|
||||
@@ -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 5–7 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")
|
||||
|
||||
@@ -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
|
||||
# ==============================================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ==============================================================================================
|
||||
#
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
# ==============================================================================================
|
||||
|
||||
@@ -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
|
||||
# ==============================================================================================
|
||||
#
|
||||
|
||||
@@ -31,6 +31,29 @@
|
||||
# NVMe any : 5–10 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
|
||||
# ==============================================================================================
|
||||
#
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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
|
||||
# ==============================================================================================
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user