Files
Varaverk/unRAID_Essentials/inotify_tuning.sh
T
Gmer4LfeandClaude Sonnet 4.6 0ae31b5fa6 feat: Tailscale resolution hardening, partnership offboard completion, Emby provisioning
common.sh:
- Add resolve_tailscale_ip() helper — tries `tailscale ip -4` first, falls back to
  parsing `tailscale status` output; handles hosts where MagicDNS short-name resolution
  is not active
- Add PARTNERSHIP_OWN_CONTAINERS alias in detect_hosts()
- Add aliasing for 4 Emby provisioning vars (PARTNERSHIP_PROVISION_EMBY_ADMIN,
  PARTNERSHIP_EMBY_ADMIN_USER, PARTNERSHIP_EMBY_ADMIN_PASS, PARTNERSHIP_EMBY_PORT)

Partnership/partnership_manager.sh:
- Replace 9 bare `tailscale ip -4` calls with resolve_tailscale_ip()
- Add read_remote_conf_var() and read_remote_conf_array() — SSH to mirror, source its
  own load_config.sh + detect_hosts(), return aliased variable; solves sparse-checkout
  problem where HOST1 cannot read master_host2.conf directly
- Add derive_short_name() — strips unraid- prefix, capitalises first char
- Add cleanup_partner_containers() — removes partner containers via FolderView3 folder
  if enabled, else falls back to FALLBACK_*_COVERS_*_TIER* arrays
- Add cleanup_owner_containers_on_mirror() — SSH to mirror, stops and removes containers
  matching *-${OWNER_SHORT} naming convention
- Add start_own_stack() and start_mirror_own_stack() — restart own containers locally
  or on mirror via SSH using PARTNERSHIP_OWN_CONTAINERS
- Add provision_emby_admin() — reads mirror credentials via read_remote_conf_var, checks
  for username collision, creates user + sets password + grants admin policy via Emby API
- Add revoke_emby_admin() — looks up mirror username on local Emby, deletes via REST API
- Wire offboard paths (both mirror-initiated and owner-initiated) to call container
  cleanup and stack restart; update --check finalisation paths accordingly
- Fix write_state_file in --onboard not gated on DRY_RUN (was writing ACTIVE state on
  dry runs)

master_host1.conf:
- Add HOST1_PARTNERSHIP_OWN_CONTAINERS array
- Add partnership Emby provisioning config (toggle + port + per-host credentials)

master_host2.conf:
- Add HOST2_PARTNERSHIP_OWN_CONTAINERS array
- Add HOST2_PARTNERSHIP_EMBY_ADMIN_USER and HOST2_PARTNERSHIP_EMBY_ADMIN_PASS

Tailscale fix applied to:
- Initial_run/ssh_setup.sh (2 callsites)
- unRAID_Essentials/rsync_stop.sh (1 callsite)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 20:09:13 -04:00

213 lines
11 KiB
Bash
Executable File

#!/bin/bash
# ==============================================================================================
# ================================= inotify Tuning ============================================
# ==============================================================================================
# Raises Linux inotify limits at array start to prevent exhaustion across the container stack.
# Run once at array start via ARRAY_START_SCRIPTS in master.conf.
# Settings are lost on reboot — this script reapplies them on every array start.
#
# ── THREE INOTIFY LIMITS ──────────────────────────────────────────────────────────────────────
# max_user_instances — max number of independent inotify file descriptor objects per user
# Each container that calls inotify_init() consumes one instance
# Default 128 — exhausted quickly with 20+ active containers
#
# max_user_watches — SHARED budget across ALL users and containers on the system
# Each watched file or directory costs one watch from this pool
# Default 8192 — VSCode alone can need 50K-200K for large workspaces
#
# max_queued_events — max events buffered before kernel starts dropping them
# Low value = events silently lost during high-activity periods
# Default 16384 — sufficient for most setups
#
# ── WHY VSCODE THROWS "UNABLE TO WATCH FOR FILE CHANGES" ─────────────────────────────────────
# VSCode (and Code-Server in Docker) opens one inotify watch per file in the workspace.
# A typical project with node_modules can easily have 100K-200K files.
# All containers on the host share max_user_watches — the combined usage of:
# Sonarr, Radarr, Lidarr, Emby, Nextcloud, Code-Server, AdGuard, all other arrs
# easily exceeds 524288 (512K) watches on a busy server.
# Raising to 1048576 (1M) gives sufficient headroom — safe on 128GB RAM (~128MB kernel use).
#
# ── STARTUP ORDER MATTERS ─────────────────────────────────────────────────────────────────────
# inotify_tuning.sh must run BEFORE containers that watch files start.
# In ARRAY_START_SCRIPTS order: inotify_tuning.sh first, then container-starting scripts.
# If Code-Server starts before limits are raised it inherits the old (low) limits.
# Code-Server restart fixes this: limits are kernel-wide, not process-bound at start.
# So if Code-Server is already running: docker restart Code-Server after this script runs.
#
# ── CONSUMERS ON THIS STACK ───────────────────────────────────────────────────────────────────
# Emby — watches all media library paths (1 watch per folder)
# Sonarr — watches TV_Shows folder tree
# Radarr — watches Movies folder tree
# Lidarr — watches Music folder tree
# Nextcloud — watches data directory for changes
# Code-Server — watches entire workspace (can be 50K-200K with node_modules)
# AdGuard Home — watches config directory
# + all other containers using inotify internally
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# acquire_lock — prevents duplicate runs at array start
# Root check — sysctl writes require root
# validate_unraid — notify validated before use
# Silent on success — runs every boot, no noise when already correct
# Only warns on changes or failures
#
# ── CONFIGURATION (master.conf) ───────────────────────────────────────────────────────────────
# INOTIFY_MAX_INSTANCES — default 1024
# INOTIFY_MAX_WATCHES — default 1048576 (1M)
# INOTIFY_MAX_QUEUED_EVENTS — default 32768
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# inotify_tuning.sh — normal run (apply settings)
# inotify_tuning.sh --dry-run — show what would change
# inotify_tuning.sh --status — show current vs target values and top consumers
# inotify_tuning.sh --log — verbose output
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root — sysctl writes require root"
exit 1
fi
validate_unraid_cmd \
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
"" "" \
"unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
acquire_lock
detect_hosts
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY INOTIFY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo ""
echo "━━━ Kernel Limits ━━━"
CURRENT_INSTANCES=$(sysctl -n fs.inotify.max_user_instances 2>/dev/null || echo "?")
CURRENT_WATCHES=$(sysctl -n fs.inotify.max_user_watches 2>/dev/null || echo "?")
CURRENT_EVENTS=$(sysctl -n fs.inotify.max_queued_events 2>/dev/null || echo "?")
for label in "max_user_instances current=$CURRENT_INSTANCES target=$INOTIFY_MAX_INSTANCES" \
"max_user_watches current=$CURRENT_WATCHES target=$INOTIFY_MAX_WATCHES" \
"max_queued_events current=$CURRENT_EVENTS target=$INOTIFY_MAX_QUEUED_EVENTS"; do
echo " $label"
done
echo ""
echo "━━━ Active Instances ━━━"
USED_INSTANCES=$(find /proc/*/fd -lname 'anon_inode:inotify' 2>/dev/null | wc -l)
USED_INSTANCES="${USED_INSTANCES//[^0-9]/}"
echo " Instances in use: ${USED_INSTANCES:-0} / $CURRENT_INSTANCES"
if [[ "$CURRENT_INSTANCES" -gt 0 ]]; then
PCT=$(( ${USED_INSTANCES:-0} * 100 / CURRENT_INSTANCES ))
echo " Utilisation: ${PCT}%"
fi
echo ""
echo "━━━ Top Consumers ━━━"
find /proc/*/fd -lname 'anon_inode:inotify' 2>/dev/null | \
awk -F/ '{print $3}' | sort | uniq -c | sort -rn | head -10 | \
while read -r count pid; do
cmd=$(cat /proc/"$pid"/comm 2>/dev/null || echo "?")
cgroup=$(cat /proc/"$pid"/cgroup 2>/dev/null | \
grep docker | grep -o '[a-f0-9]\{12\}' | head -1 || echo "")
if [[ -n "$cgroup" ]]; then
label="[docker:${cgroup}] $cmd"
else
label="[host] $cmd"
fi
echo " ${count} instances — $label (PID $pid)"
done | head -10
echo ""
echo "━━━ VSCode / Code-Server ━━━"
echo " If VSCode shows 'unable to watch for file changes':"
echo " 1. Verify max_user_watches target is set high enough"
echo " 2. Check total watches used: cat /proc/sys/fs/inotify/max_user_watches"
echo " 3. After any limit change: docker restart Code-Server"
echo " (running containers inherit limits at start, not dynamically)"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ==============================================================================================
# ━━━ Apply Settings ━━━
# ==============================================================================================
CHANGED=0
FAILED=0
apply_sysctl() {
local key="$1" value="$2"
local current
current=$(sysctl -n "$key" 2>/dev/null || echo 0)
if [[ "$current" -eq "$value" ]]; then
log "$key = $value (already correct)"
return 0
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would set $key = $value (currently $current)"
return 0
fi
if sysctl -w "${key}=${value}" >/dev/null 2>&1; then
warn "Set $key = $value (was $current)"
(( CHANGED++ ))
else
error "Failed to set $key = $value"
(( FAILED++ ))
fi
}
apply_sysctl "fs.inotify.max_user_instances" "$INOTIFY_MAX_INSTANCES"
apply_sysctl "fs.inotify.max_user_watches" "$INOTIFY_MAX_WATCHES"
apply_sysctl "fs.inotify.max_queued_events" "$INOTIFY_MAX_QUEUED_EVENTS"
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
if [[ "$FAILED" -gt 0 ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY INOTIFY TUNING SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_ERROR $FAILED setting(s) failed to apply"
notify "inotify tuning failed on $(hostname) ($MY_ID) — $FAILED setting(s) could not be applied" \
"inotify Tuning" "warning"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 1
elif [[ "$CHANGED" -gt 0 ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY INOTIFY TUNING SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo " max_user_instances: $(sysctl -n fs.inotify.max_user_instances 2>/dev/null)"
echo " max_user_watches: $(sysctl -n fs.inotify.max_user_watches 2>/dev/null)"
echo " max_queued_events: $(sysctl -n fs.inotify.max_queued_events 2>/dev/null)"
echo ""
warn "$CHANGED setting(s) updated"
if [[ "$CHANGED" -gt 0 ]]; then
warn "If Code-Server is running: docker restart Code-Server"
warn "Running containers inherit limits at start — restart picks up new values"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
else
# Already correct — completely silent (runs every boot)
log "inotify limits already correct — no changes needed"
fi
exit 0