massive update. Master conf split, now modular with a load sceriprt to drive all configs to scripts. with unraid scpecific safeguard tests , and improved standardized ux. including dynamic host detect, who am i who else it there. EVERY SINGLE SCRIPT UPDATED. DEBATING THAT THIS IS ACUALLY V2

This commit is contained in:
2026-05-03 17:16:49 -04:00
parent 2691a35e80
commit ec7de648dc
72 changed files with 25640 additions and 14629 deletions
+199 -84
View File
@@ -1,50 +1,79 @@
#!/bin/bash
# -----------------------------------------------------------------------------------------------
# --------------------------------- Container Data Export --------------------------------------
# -----------------------------------------------------------------------------------------------
# ==============================================================================================
# ============================= Container Data Export ==========================================
# ==============================================================================================
# Exports a container's appdata directory to a compressed tar archive.
# Stops the container before archiving and restarts it after — ensures clean consistent backup.
# Verifies the archive after creation — confirms backup is valid before restarting container.
#
# Usage:
# ── WHEN TO USE ───────────────────────────────────────────────────────────────────────────────
# - Before major container updates (roll back if update goes wrong)
# - Before pool migrations or disk replacements
# - When archiving a container being removed from the stack
# - Before destructive operations on appdata (database migrations etc.)
# - One-off backup of a specific container without running full backup
#
# ── OUTPUT FILE NAMING ────────────────────────────────────────────────────────────────────────
# ContainerName_YYYY-MM-DD_HH-MM.tar.gz
# Timestamp in filename — run multiple times safely, no overwrite ✅
#
# ── SPACE CHECK ───────────────────────────────────────────────────────────────────────────────
# Estimates required space as appdata size × 1.1 (10% buffer).
# Compressed archive will typically be much smaller — this is a conservative floor.
# gzip compression ratio depends heavily on content — database files compress well,
# media files do not. If output is on a media share estimate may be pessimistic.
#
# ── ARCHIVE VERIFICATION ──────────────────────────────────────────────────────────────────────
# After creation the archive is tested with tar --test-file before restarting the container.
# If verification fails the container is still restarted (data unchanged) and an error logged.
# A corrupt archive is not a usable backup — do not assume the archive is good without this.
#
# ── SAFEGUARDS ────────────────────────────────────────────────────────────────────────────────
# DOCKER_TIMEOUT — docker calls protected against hung daemon
# Container restart rule — was running → restart | was stopped → leave stopped ✅
# Archive cleanup — partial archive removed on tar failure
# Archive verification — tar --test-file after creation
# Container restart on — any failure path still restarts container if it was running
# validate_unraid_cmd — notify validated before use
# Silent on success — only problems produce visible output
#
# ── USAGE ─────────────────────────────────────────────────────────────────────────────────────
# container_data_export.sh ContainerName /path/to/appdata /path/to/output/dir
# container_data_export.sh Emby /mnt/media-servers/Media_Server/Emby /mnt/user/Backups/
#
# Output file naming:
# ContainerName_YYYY-MM-DD_HH-MM.tar.gz
#
# Use before major container updates, pool migrations, or when archiving
# a container you are removing from the stack.
#
# Supports --dry-run to show what would be archived without making changes.
# -----------------------------------------------------------------------------------------------
# container_data_export.sh Emby /mnt/media-servers/Media_Server/Emby /mnt/user/Backups/ --dry-run
# container_data_export.sh Emby /mnt/media-servers/Media_Server/Emby /mnt/user/Backups/ --log
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../Master.conf"
source "$SCRIPT_DIR/../common.sh"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
# -----------------------------------------------------------------------------------------------
# Args
# -----------------------------------------------------------------------------------------------
DOCKER_TIMEOUT=30 # longer timeout — stop can take time on large containers
# ── Positional args ───────────────────────────────────────────────────────────────────────────
CONTAINER_NAME="${PARSED_ARGS[0]:-}"
APPDATA_PATH="${PARSED_ARGS[1]:-}"
OUTPUT_DIR="${PARSED_ARGS[2]:-}"
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_GEAR Setup ━━━
# -----------------------------------------------------------------------------------------------
echo ""
echo "━━━ $ICON_GEAR Setup ━━━"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
success "Running as root"
validate_unraid_cmd \
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
"" "" \
"unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
# detect_hosts() sets MY_ID — used in summary
detect_hosts
# Arg validation
if [[ -z "$CONTAINER_NAME" || -z "$APPDATA_PATH" || -z "$OUTPUT_DIR" ]]; then
error "Usage: container_data_export.sh <ContainerName> <appdata_path> <output_dir>"
error "Example: container_data_export.sh Emby /mnt/media-servers/Media_Server/Emby /mnt/user/Backups/"
@@ -58,55 +87,94 @@ fi
if [[ ! -d "$OUTPUT_DIR" ]]; then
error "Output directory not found: $OUTPUT_DIR"
error "Create it first: mkdir -p \"$OUTPUT_DIR\""
exit 1
fi
# Check free space — rough estimate: appdata size × 1.1
# Space check — conservative: appdata × 1.1
APPDATA_SIZE_KB=$(du -sk "$APPDATA_PATH" 2>/dev/null | cut -f1)
OUTPUT_FREE_KB=$(df "$OUTPUT_DIR" --output=avail | tail -1 | tr -d ' ')
OUTPUT_FREE_KB=$(df "$OUTPUT_DIR" --output=avail 2>/dev/null | tail -1 | tr -d ' ')
REQUIRED_KB=$(( APPDATA_SIZE_KB * 11 / 10 ))
APPDATA_SIZE_H=$(du -sh "$APPDATA_PATH" 2>/dev/null | cut -f1)
OUTPUT_FREE_H=$(df -h "$OUTPUT_DIR" --output=avail | tail -1 | tr -d ' ')
info "Container: $CONTAINER_NAME"
info "Appdata: $APPDATA_PATH ($APPDATA_SIZE_H)"
info "Output dir: $OUTPUT_DIR ($OUTPUT_FREE_H free)"
OUTPUT_FREE_H=$(df -h "$OUTPUT_DIR" --output=avail 2>/dev/null | tail -1 | tr -d ' ')
if [[ "$OUTPUT_FREE_KB" -lt "$REQUIRED_KB" ]]; then
error "Insufficient space in $OUTPUT_DIR — need ~${APPDATA_SIZE_H}, have ${OUTPUT_FREE_H}"
error "Insufficient space in $OUTPUT_DIR"
error "Estimated need: ~${APPDATA_SIZE_H} (×1.1 conservative) — available: ${OUTPUT_FREE_H}"
exit 1
fi
success "Space check passed"
log "Container: $CONTAINER_NAME"
log "Appdata: $APPDATA_PATH ($APPDATA_SIZE_H)"
log "Output: $OUTPUT_DIR ($OUTPUT_FREE_H free)"
log "Space check passed"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_STOP Stop Container ━━━
# -----------------------------------------------------------------------------------------------
# ── Ensure container is restarted on any exit if it was running ────────────────────────────────
CONTAINER_WAS_RUNNING=false
ARCHIVE_PATH=""
cleanup_on_exit() {
local exit_code=$?
# Remove partial archive on failure
if [[ "$exit_code" -ne 0 && -n "$ARCHIVE_PATH" && -f "$ARCHIVE_PATH" ]]; then
warn "Removing partial archive: $ARCHIVE_PATH"
rm -f "$ARCHIVE_PATH" 2>/dev/null
fi
# Always restart container if it was running
if [[ "$CONTAINER_WAS_RUNNING" == true && "$DRY_RUN" == false ]]; then
local status
status=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
"$CONTAINER_NAME" 2>/dev/null)
if [[ "$status" != "true" ]]; then
warn "Restarting $CONTAINER_NAME (cleanup)..."
timeout "$DOCKER_TIMEOUT" docker start "$CONTAINER_NAME" >/dev/null 2>&1 || \
error "Failed to restart $CONTAINER_NAME — start it manually"
fi
fi
}
trap cleanup_on_exit EXIT
# ==============================================================================================
# ━━━ Stop Container ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_STOP Stop Container ━━━"
CONTAINER_WAS_RUNNING=false
STATUS=$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)
STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f '{{.State.Running}}' \
"$CONTAINER_NAME" 2>/dev/null)
if [[ "$STATUS" == "true" ]]; then
CONTAINER_WAS_RUNNING=true
info "$ICON_STOP Stopping $CONTAINER_NAME for clean export..."
if [[ "$DRY_RUN" == false ]]; then
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 && \
success "$ICON_STOPPED $CONTAINER_NAME stopped" || \
{ error "Failed to stop $CONTAINER_NAME"; exit 1; }
else
warn "DRY RUN — would stop $CONTAINER_NAME"
fi
else
info "$CONTAINER_NAME is not running — archiving as-is"
fi
case "$STATUS" in
true)
CONTAINER_WAS_RUNNING=true
log "Stopping $CONTAINER_NAME for clean export..."
if [[ "$DRY_RUN" == false ]]; then
if timeout "$DOCKER_TIMEOUT" docker stop "$CONTAINER_NAME" >/dev/null 2>&1; then
log "$CONTAINER_NAME stopped ✅"
else
error "Failed to stop $CONTAINER_NAME — aborting export"
exit 1
fi
else
warn "DRY RUN — would stop $CONTAINER_NAME"
fi
;;
false)
log "$CONTAINER_NAME is not running — archiving as-is (was stopped state respected)"
;;
"")
error "$CONTAINER_NAME not found — check container name"
exit 1
;;
*)
warn "$CONTAINER_NAME status: $STATUS — proceeding with caution"
;;
esac
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_SYNC Archive ━━━
# -----------------------------------------------------------------------------------------------
# ==============================================================================================
# ━━━ Archive ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_SYNC Archive ━━━"
@@ -114,60 +182,107 @@ TIMESTAMP=$(date '+%Y-%m-%d_%H-%M')
ARCHIVE_NAME="${CONTAINER_NAME}_${TIMESTAMP}.tar.gz"
ARCHIVE_PATH="${OUTPUT_DIR}/${ARCHIVE_NAME}"
info "Creating: $ARCHIVE_PATH"
warn "Creating: $ARCHIVE_PATH"
warn "Source: $APPDATA_PATH ($APPDATA_SIZE_H)"
START=$(date +%s)
ARCHIVE_VERIFIED=false
if [[ "$DRY_RUN" == false ]]; then
tar -czf "$ARCHIVE_PATH" -C "$(dirname "$APPDATA_PATH")" "$(basename "$APPDATA_PATH")" 2>/dev/null
TAR_EXIT=$?
if tar -czf "$ARCHIVE_PATH" \
-C "$(dirname "$APPDATA_PATH")" \
"$(basename "$APPDATA_PATH")" 2>/dev/null; then
if [[ "$TAR_EXIT" -ne 0 ]]; then
error "Archive failed (exit code $TAR_EXIT)"
# Restart container before exiting
[[ "$CONTAINER_WAS_RUNNING" == true ]] && docker start "$CONTAINER_NAME" >/dev/null 2>&1
ARCHIVE_SIZE=$(du -sh "$ARCHIVE_PATH" 2>/dev/null | cut -f1)
warn "Archive created: $ARCHIVE_NAME ($ARCHIVE_SIZE)"
# Verify archive integrity before declaring success
log "Verifying archive..."
if tar --test-label -f "$ARCHIVE_PATH" 2>/dev/null || \
tar -tzf "$ARCHIVE_PATH" >/dev/null 2>&1; then
log "Archive verified ✅"
ARCHIVE_VERIFIED=true
else
error "Archive verification FAILED — archive may be corrupt"
error "Container will be restarted but DO NOT rely on this backup"
notify "Container export archive corrupt — $CONTAINER_NAME backup may be unusable" \
"Container Export" "warning"
fi
else
error "tar failed — archive creation unsuccessful"
exit 1
fi
ARCHIVE_SIZE=$(du -sh "$ARCHIVE_PATH" 2>/dev/null | cut -f1)
success "Archive created: $ARCHIVE_NAME ($ARCHIVE_SIZE)"
else
warn "DRY RUN — would create: $ARCHIVE_PATH"
ARCHIVE_VERIFIED=true
fi
END=$(date +%s)
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_START Restart Container ━━━
# -----------------------------------------------------------------------------------------------
# ==============================================================================================
# ━━━ Restart Container ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_START Restart Container ━━━"
RESTART_OK=false
if [[ "$CONTAINER_WAS_RUNNING" == true ]]; then
info "$ICON_START Restarting $CONTAINER_NAME..."
log "Restarting $CONTAINER_NAME..."
if [[ "$DRY_RUN" == false ]]; then
docker start "$CONTAINER_NAME" >/dev/null 2>&1 && \
success "$ICON_STARTED $CONTAINER_NAME restarted" || \
if timeout "$DOCKER_TIMEOUT" docker start "$CONTAINER_NAME" >/dev/null 2>&1; then
# Brief settle then verify
sleep 3
POST_STATUS=$(timeout "$DOCKER_TIMEOUT" docker inspect -f \
'{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)
if [[ "$POST_STATUS" == "true" ]]; then
log "$CONTAINER_NAME restarted and running ✅"
RESTART_OK=true
else
error "$CONTAINER_NAME started but crashed immediately — check container logs"
notify "$CONTAINER_NAME failed to stay running after export on $(hostname)" \
"Container Export" "warning"
fi
else
error "Failed to restart $CONTAINER_NAME — start it manually"
notify "$CONTAINER_NAME failed to restart after export on $(hostname)" \
"Container Export" "warning"
fi
else
warn "DRY RUN — would restart $CONTAINER_NAME"
RESTART_OK=true
fi
else
info "$CONTAINER_NAME was not running — not restarting"
log "$CONTAINER_NAME was not running — leaving stopped (state respected) ✅"
RESTART_OK=true
fi
# -----------------------------------------------------------------------------------------------
# ━━━ $ICON_SUMMARY Summary ━━━
# -----------------------------------------------------------------------------------------------
# Clear trap — clean exit, cleanup_on_exit no longer needed
trap - EXIT
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY CONTAINER EXPORT SUMMARY ━━━━━"
echo "$ICON_CONTAINERS Container: $CONTAINER_NAME"
echo "$ICON_DISK Appdata: $APPDATA_PATH ($APPDATA_SIZE_H)"
echo "$ICON_SYNC Archive: ${ARCHIVE_NAME:-DRY RUN} ${ARCHIVE_SIZE:+($ARCHIVE_SIZE)}"
echo "$ICON_TIME Duration: $(format_duration $((END - START)))"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_CONTAINERS Container: $CONTAINER_NAME"
echo "$ICON_DISK Appdata: $APPDATA_PATH ($APPDATA_SIZE_H)"
echo "$ICON_SYNC Archive: ${ARCHIVE_NAME:-DRY RUN} ${ARCHIVE_SIZE:+($ARCHIVE_SIZE)}"
echo "$ICON_SHIELD Verified: $([[ "$ARCHIVE_VERIFIED" == true ]] && echo "✅" || echo "❌ FAILED")"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo ""
if [[ "$DRY_RUN" == true ]]; then
echo "$ICON_WARN Status: DRY RUN — no changes made"
warn "DRY RUN — no changes made"
elif [[ "$ARCHIVE_VERIFIED" == true && "$RESTART_OK" == true ]]; then
log "$ICON_DONE Status: done — $ARCHIVE_NAME"
elif [[ "$ARCHIVE_VERIFIED" == false ]]; then
echo "$ICON_ERROR Status: archive verification FAILED — check backup before relying on it"
else
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
notify "Container export complete — $CONTAINER_NAME archived to $ARCHIVE_NAME" "Container Export" "normal"
warn "Status: complete with warnings — check restart status above"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[[ "$ARCHIVE_VERIFIED" == false ]] && exit 1
exit 0