#!/usr/bin/env bash set -euo pipefail # =================================================================== # felhom-wipe.sh — Clean felhom data from a test node # Usage: ./felhom-wipe.sh --level [--yes] # =================================================================== # --- Colors --- RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # --- Defaults --- LEVEL="" DRY_RUN=true INCLUDE_PROTECTED=false # --- Configuration (auto-detected) --- CONTROLLER_YAML="/opt/docker/felhom-controller/controller.yaml" DATA_DIR="/opt/docker/felhom-controller/data" COMPOSE_DIR="/opt/docker/felhom-controller" STACKS_DIR="/opt/docker/stacks" SETTINGS_JSON="$DATA_DIR/settings.json" # --- Helpers --- die() { echo -e "${RED}ERROR: $1${NC}" >&2; exit 1; } info() { echo -e "${GREEN}$1${NC}"; } warn() { echo -e "${YELLOW}$1${NC}"; } bold() { echo -e "${BOLD}$1${NC}"; } human_size() { local path="$1" if [ -e "$path" ]; then du -sh "$path" 2>/dev/null | cut -f1 || echo "?" else echo "n/a" fi } usage() { cat < [--yes] [--include-protected] Levels: soft Controller state only (settings.json, metrics.db, session data) controller Soft + remove all app containers, volumes, stack dirs, app.yaml files full Controller + felhom-data/ on all drives (appdata, backups) nuclear Full + controller.yaml, controller container, Traefik, Portainer, all Docker data Options: --yes Execute the wipe (default: dry run) --include-protected Also remove protected stacks (controller level only) EOF exit 1 } # --- Parse Args --- parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --level) LEVEL="$2"; shift 2 ;; --yes) DRY_RUN=false; shift ;; --include-protected) INCLUDE_PROTECTED=true; shift ;; -h|--help) usage ;; *) die "Unknown argument: $1" ;; esac done [[ -z "$LEVEL" ]] && usage case "$LEVEL" in soft|controller|full|nuclear) ;; *) die "Invalid level: $LEVEL (must be soft|controller|full|nuclear)" ;; esac } # --- Detect Paths --- detect_paths() { # Auto-detect from controller.yaml if readable (may be root-owned) if [ -f "$CONTROLLER_YAML" ] && [ -r "$CONTROLLER_YAML" ]; then local sd sd=$(grep -oP 'stacks_dir:\s*\K\S+' "$CONTROLLER_YAML" 2>/dev/null || true) [ -n "$sd" ] && STACKS_DIR="$sd" local dd dd=$(grep -oP 'data_dir:\s*\K\S+' "$CONTROLLER_YAML" 2>/dev/null || true) [ -n "$dd" ] && DATA_DIR="$dd" && SETTINGS_JSON="$dd/settings.json" fi # If settings.json not found at configured path, try the Docker volume directly. # The controller stores data in a named volume (felhom-controller_controller-data), # not at the container-internal path on the host filesystem. if [ ! -f "$SETTINGS_JSON" ]; then local vol_path vol_path=$(docker volume inspect felhom-controller_controller-data --format '{{.Mountpoint}}' 2>/dev/null || true) if [ -n "$vol_path" ] && [ -d "$vol_path" ]; then DATA_DIR="$vol_path" SETTINGS_JSON="$vol_path/settings.json" fi fi } # --- Detect Storage Paths --- declare -a STORAGE_PATHS=() detect_storage_paths() { # From settings.json if [ -f "$SETTINGS_JSON" ]; then while IFS= read -r p; do [ -n "$p" ] && STORAGE_PATHS+=("$p") done < <(python3 -c " import json, sys try: d = json.load(open('$SETTINGS_JSON')) for sp in d.get('storage_paths', []): print(sp.get('path', '')) except: pass " 2>/dev/null || true) fi # Also scan /mnt/* for felhom-managed dirs not in registry. # Detect by presence of felhom-data/, legacy appdata/backups/, or .felhom-infra-backup/ for d in /mnt/*/; do [ -d "${d}felhom-data" ] || [ -d "${d}appdata" ] || [ -d "${d}backups" ] \ || [ -d "${d}.felhom-infra-backup" ] || continue local already=false for sp in "${STORAGE_PATHS[@]:-}"; do [ "$sp" = "${d%/}" ] && already=true && break done $already || STORAGE_PATHS+=("${d%/}") done } # --- List App Containers (non-infra) --- list_app_containers() { docker ps -a --format '{{.Names}}' 2>/dev/null | grep -v -E '^(felhom-controller|traefik|cloudflared|portainer)$' || true } # --- List App Volumes (non-infra) --- list_app_volumes() { docker volume ls -q 2>/dev/null | grep -v -E '^(portainer_data)$' || true } # --- Protected Stacks --- get_protected_stacks() { if [ -f "$CONTROLLER_YAML" ]; then grep -A 20 'protected_stacks:' "$CONTROLLER_YAML" 2>/dev/null | grep -oP '^\s*-\s*\K\S+' || true fi } # --- Print Plan --- print_plan() { echo "" bold "=== Felhom Wipe — Level: $LEVEL ===" echo "" # State files echo -e "${CYAN}Controller state:${NC}" local state_files=("$DATA_DIR/settings.json" "$DATA_DIR/metrics.db" "$DATA_DIR/setup-state.json" "$DATA_DIR/update-state.json" "$DATA_DIR/session-data.json" "$DATA_DIR/snapshot-history.json") for f in "${state_files[@]}"; do if [ -f "$f" ]; then echo -e " ${YELLOW}DELETE${NC} $f ($(human_size "$f"))" fi done if [[ "$LEVEL" == "controller" || "$LEVEL" == "full" || "$LEVEL" == "nuclear" ]]; then echo "" echo -e "${CYAN}Docker containers:${NC}" local containers containers=$(list_app_containers) if [ -n "$containers" ]; then echo "$containers" | while read -r c; do echo -e " ${YELLOW}REMOVE${NC} $c" done else echo -e " ${GREEN}(none)${NC}" fi echo "" echo -e "${CYAN}Docker volumes:${NC}" local volumes volumes=$(list_app_volumes) if [ -n "$volumes" ]; then echo "$volumes" | while read -r v; do echo -e " ${YELLOW}REMOVE${NC} $v" done else echo -e " ${GREEN}(none)${NC}" fi echo "" echo -e "${CYAN}Stack directories:${NC}" if [ -d "$STACKS_DIR" ]; then for sd in "$STACKS_DIR"/*/; do [ -d "$sd" ] || continue local stack_name stack_name=$(basename "$sd") local protected_stacks protected_stacks=$(get_protected_stacks) if echo "$protected_stacks" | grep -qx "$stack_name" && ! $INCLUDE_PROTECTED; then echo -e " ${GREEN}KEEP${NC} $sd (protected)" else echo -e " ${YELLOW}DELETE${NC} $sd" fi done else echo -e " ${GREEN}(not found)${NC}" fi fi if [[ "$LEVEL" == "full" || "$LEVEL" == "nuclear" ]]; then echo "" echo -e "${CYAN}Mount cleanup:${NC}" if [ -d /mnt/.felhom-scan ]; then echo -e " ${YELLOW}DELETE${NC} /mnt/.felhom-scan/ (stale scan dir)" else echo -e " ${GREEN}(no .felhom-scan dir)${NC}" fi fi if [[ "$LEVEL" == "full" || "$LEVEL" == "nuclear" ]]; then echo "" echo -e "${CYAN}Storage data:${NC}" if [ ${#STORAGE_PATHS[@]} -gt 0 ]; then for sp in "${STORAGE_PATHS[@]}"; do if [ -d "$sp/felhom-data" ]; then echo -e " ${YELLOW}DELETE${NC} $sp/felhom-data/ ($(human_size "$sp/felhom-data"))" fi # Old-style paths if [ -d "$sp/appdata" ]; then echo -e " ${YELLOW}DELETE${NC} $sp/appdata/ ($(human_size "$sp/appdata")) [old-style]" fi if [ -d "$sp/backups" ]; then echo -e " ${YELLOW}DELETE${NC} $sp/backups/ ($(human_size "$sp/backups")) [old-style]" fi done else echo -e " ${GREEN}(no storage paths found)${NC}" fi fi if [[ "$LEVEL" == "nuclear" ]]; then echo "" echo -e "${RED}Nuclear:${NC}" echo -e " ${RED}DELETE${NC} controller.yaml" echo -e " ${RED}DELETE${NC} controller container + image" echo -e " ${RED}DELETE${NC} Traefik container" echo -e " ${RED}DELETE${NC} Cloudflared container" echo -e " ${RED}DELETE${NC} Portainer container + volume" echo -e " ${RED}DELETE${NC} .felhom-infra-backup/ (DR markers on detected drives):" local dr_found=false for sp in "${STORAGE_PATHS[@]:-}"; do if [ -d "$sp/.felhom-infra-backup" ]; then echo -e " ${RED}DELETE${NC} $sp/.felhom-infra-backup/" dr_found=true fi done $dr_found || echo -e " ${GREEN}(none found)${NC}" if [ -d /mnt/.felhom-raw ]; then echo -e " ${RED}UNMOUNT+DELETE${NC} /mnt/.felhom-raw/ (raw helper mounts + fstab entries)" # Show each raw mount and its bind target for rmp in /mnt/.felhom-raw/*/; do [ -d "$rmp" ] || continue local label; label=$(basename "$rmp") local bind_target bind_target=$(grep -E "^/mnt/\.felhom-raw/${label}/" /etc/fstab 2>/dev/null | awk '{print $2}' | head -1 || true) if [ -n "$bind_target" ]; then echo -e " ${RED}umount + rmdir${NC} ${bind_target} (bind) → ${rmp} (raw)" else echo -e " ${RED}umount${NC} ${rmp} (raw, no bind found)" fi done fi echo -e " ${RED}DELETE${NC} All Docker data (docker system prune -af --volumes)" echo -e " ${RED}DELETE${NC} $COMPOSE_DIR/ (controller compose + .env)" local infra_root; infra_root=$(dirname "$COMPOSE_DIR") echo -e " ${RED}DELETE${NC} $infra_root/traefik/ (configs + acme.json)" echo -e " ${RED}DELETE${NC} $infra_root/cloudflared/ (configs)" echo -e " ${RED}DELETE${NC} $STACKS_DIR/ (empty stacks dir)" fi echo "" echo -e "${CYAN}Will preserve:${NC}" echo -e " ${GREEN}- OS and system files${NC}" if [[ "$LEVEL" != "nuclear" ]]; then echo -e " ${GREEN}- Controller container (felhom-controller)${NC}" echo -e " ${GREEN}- Controller image${NC}" echo -e " ${GREEN}- Traefik, Cloudflare Tunnel${NC}" echo -e " ${GREEN}- controller.yaml${NC}" echo -e " ${GREEN}- .felhom-infra-backup/ (DR markers on drives)${NC}" fi if [[ "$LEVEL" != "full" && "$LEVEL" != "nuclear" ]]; then echo -e " ${GREEN}- Storage data on drives${NC}" fi echo -e " ${GREEN}- User files (Dokumentumok, media, etc.)${NC}" echo "" } # --- Mount Cleanup Helpers --- # cleanup_scan_dir: remove /mnt/.felhom-scan/ (ephemeral DR scan staging dir). # Always empty after normal operation; safe to rm -rf unconditionally. cleanup_scan_dir() { if [ -d /mnt/.felhom-scan ]; then rm -rf /mnt/.felhom-scan && info " Removed: /mnt/.felhom-scan/" fi } # cleanup_raw_mounts: unmount bind mounts, unmount raw helper mounts, strip # /etc/fstab entries, then remove the now-empty /mnt/.felhom-raw/ directory. # # Raw mounts are created by the attach wizard (two-level: raw partition mount + # bind mount from subfolder). Both fstab entries must be removed so they don't # cause errors on next boot. Order: bind umount first, then raw umount. cleanup_raw_mounts() { [ -d /mnt/.felhom-raw ] || return info "Cleaning up raw helper mounts (/mnt/.felhom-raw/)..." # 1. Unmount bind mounts whose source is inside .felhom-raw (field 1 matches) # Collect targets first so we can rmdir them after unmounting. local bind_targets="" if [ -f /etc/fstab ]; then bind_targets=$(grep -E '^/mnt/\.felhom-raw/' /etc/fstab | awk '{print $2}' || true) for mp in $bind_targets; do if mountpoint -q "$mp" 2>/dev/null; then umount -l "$mp" 2>/dev/null && info " Unmounted bind: $mp" \ || warn " Could not unmount bind: $mp" fi done fi # 2. Unmount raw partition mounts (field 2 matches /mnt/.felhom-raw/*) for mp in /mnt/.felhom-raw/*/; do [ -d "$mp" ] || continue if mountpoint -q "$mp" 2>/dev/null; then umount -l "$mp" 2>/dev/null && info " Unmounted raw: $mp" \ || warn " Could not unmount raw: $mp" fi done # 3. Strip all .felhom-raw entries from fstab (both raw and bind lines) if [ -f /etc/fstab ] && grep -q '\.felhom-raw' /etc/fstab 2>/dev/null; then sed -i '\|\.felhom-raw|d' /etc/fstab && info " Removed .felhom-raw entries from /etc/fstab" fi # 4. Remove /mnt/.felhom-raw/ — safe now that mounts are gone rm -rf /mnt/.felhom-raw && info " Removed: /mnt/.felhom-raw/" # 5. Remove the now-empty bind target directories (e.g. /mnt/hdd_1). # Use rmdir so we only remove them if truly empty — never touch user data. for mp in $bind_targets; do if [ -d "$mp" ]; then rmdir "$mp" 2>/dev/null && info " Removed mount point: $mp" \ || warn " Could not remove (not empty?): $mp" fi done } # --- Wipe Functions --- do_soft_wipe() { info "Soft wipe: removing controller state..." local state_files=("$DATA_DIR/settings.json" "$DATA_DIR/metrics.db" "$DATA_DIR/setup-state.json" "$DATA_DIR/update-state.json" "$DATA_DIR/session-data.json" "$DATA_DIR/snapshot-history.json") for f in "${state_files[@]}"; do if [ -f "$f" ]; then rm -f "$f" && info " Removed: $f"; fi done } do_controller_wipe() { do_soft_wipe info "Controller wipe: stopping and removing app containers..." # Stop and remove app containers local containers containers=$(list_app_containers) if [ -n "$containers" ]; then echo "$containers" | while read -r c; do docker rm -f "$c" 2>/dev/null && info " Removed container: $c" || warn " Failed to remove: $c" done fi # Remove app volumes info "Removing app volumes..." local volumes volumes=$(list_app_volumes) if [ -n "$volumes" ]; then echo "$volumes" | while read -r v; do docker volume rm "$v" 2>/dev/null && info " Removed volume: $v" || warn " Failed to remove: $v" done fi # Remove stack directories info "Removing stack directories..." if [ -d "$STACKS_DIR" ]; then local protected_stacks protected_stacks=$(get_protected_stacks) for sd in "$STACKS_DIR"/*/; do [ -d "$sd" ] || continue local stack_name stack_name=$(basename "$sd") if echo "$protected_stacks" | grep -qx "$stack_name" && ! $INCLUDE_PROTECTED; then warn " Skipping protected stack: $stack_name" continue fi rm -rf "$sd" && info " Removed: $sd" done fi # NOTE: No restart here — callers handle restart after all cleanup is done. } do_full_wipe() { do_controller_wipe info "Full wipe: removing storage data..." for sp in "${STORAGE_PATHS[@]}"; do # New-style namespace if [ -d "$sp/felhom-data" ]; then rm -rf "$sp/felhom-data" && info " Removed: $sp/felhom-data/" fi # Old-style paths if [ -d "$sp/appdata" ]; then rm -rf "$sp/appdata" && info " Removed: $sp/appdata/ [old-style]" fi if [ -d "$sp/backups" ]; then rm -rf "$sp/backups" && info " Removed: $sp/backups/ [old-style]" fi done # Remove stale scan dir (ephemeral DR staging — always safe to remove) cleanup_scan_dir # Restart controller after all cleanup is done info "Restarting controller..." docker restart felhom-controller 2>/dev/null || warn "Could not restart controller" } do_nuclear_wipe() { do_full_wipe info "Nuclear wipe: removing all infrastructure..." # Stop infrastructure containers for c in felhom-controller traefik cloudflared portainer; do docker rm -f "$c" 2>/dev/null && info " Removed: $c" || true done # Remove DR markers (nuclear = brand-new machine simulation) for sp in "${STORAGE_PATHS[@]}"; do if [ -d "$sp/.felhom-infra-backup" ]; then rm -rf "$sp/.felhom-infra-backup" && info " Removed: $sp/.felhom-infra-backup/" fi done # Unmount raw helper mounts, strip fstab entries, remove dirs # (scan dir already removed by do_full_wipe above) cleanup_raw_mounts # Remove all Docker data warn "Pruning all Docker data..." docker system prune -af --volumes 2>/dev/null || warn "Docker prune failed" # Remove infra config directories (after prune so containers are gone) local infra_root infra_root=$(dirname "$COMPOSE_DIR") rm -rf "$COMPOSE_DIR" && info " Removed: $COMPOSE_DIR/" rm -rf "$infra_root/traefik" && info " Removed: $infra_root/traefik/" rm -rf "$infra_root/cloudflared" && info " Removed: $infra_root/cloudflared/" rm -rf "$STACKS_DIR" && info " Removed: $STACKS_DIR/" # Remove any leftover empty mount point dirs under /mnt/ (e.g. /mnt/hdd_1 after # the bind mount was already cleaned by a prior wipe run). rmdir is safe: it # refuses to remove non-empty directories, so user data is never at risk. for mp in /mnt/*/; do [ -d "$mp" ] || continue rmdir "$mp" 2>/dev/null && info " Removed empty mount point: $mp" || true done echo "" info "Nuclear wipe complete." echo -e "${CYAN}To redeploy, run:${NC}" echo " curl -fsSL https://gitea.dooplex.hu/admin/deploy-felhom-compose/raw/branch/main/scripts/docker-setup.sh | bash" } # --- Main --- main() { # Must run as root if [ "$(id -u)" -ne 0 ]; then die "Must run as root (use sudo)" fi # Check Docker if ! docker info >/dev/null 2>&1; then die "Docker is not running" fi parse_args "$@" detect_paths detect_storage_paths print_plan if $DRY_RUN; then warn "Dry run — nothing deleted. Use --yes to execute." exit 0 fi # Confirmation echo -e "${RED}${BOLD}This will permanently delete the data listed above.${NC}" read -rp "Type YES to confirm: " confirm if [ "$confirm" != "YES" ]; then echo "Aborted." exit 1 fi echo "" case "$LEVEL" in soft) do_soft_wipe ;; controller) do_controller_wipe info "Restarting controller..." docker restart felhom-controller 2>/dev/null || warn "Could not restart controller" ;; full) do_full_wipe ;; nuclear) do_nuclear_wipe ;; esac echo "" info "Wipe complete (level: $LEVEL)." } main "$@"