v0.26.0: Storage namespace felhom-data/ + test node wipe script
All felhom-managed data on external drives now lives under felhom-data/ subdirectory, cleanly separating controller data from user files. - backup/paths.go: add FelhomDataDir constant, update 8 path helpers - stacks/delete.go: add local felhomDataDir constant (circular import boundary), update ProtectedHDDPaths + GetStackBackupData - storage/migrate_drive.go: import backup pkg, fix conflict check, verify, rsync excludes (felhom-data/backups/*/restic/), size estimation - storage/migrate.go: import backup pkg, fix DB dump paths - web/handlers.go: fix legacy 'storage' path -> backup.AppDataDir() - storage/format_linux.go: create felhom-data/ instead of storage/ - storage/attach_linux.go: create felhom-data/ instead of storage/ - scripts/felhom-wipe.sh: new multi-level test node wipe script (soft/controller/full/nuclear) - CHANGELOG.md, controller/README.md, scripts/README.md: updated docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -195,3 +195,65 @@ dprune='sudo docker system prune -af'
|
||||
|
||||
- **ctop** — Top-like interface for container metrics
|
||||
- **lazydocker** — Terminal UI for Docker management
|
||||
|
||||
---
|
||||
|
||||
## felhom-wipe.sh
|
||||
|
||||
**Test node cleanup script with 4 wipe levels.**
|
||||
|
||||
Removes felhom-managed data from a node in a controlled, repeatable way. Designed for test/demo nodes to reset state between testing cycles.
|
||||
|
||||
### Quick start
|
||||
|
||||
```bash
|
||||
# Preview what will be removed (dry run — default)
|
||||
sudo ./felhom-wipe.sh --level full
|
||||
|
||||
# Execute the wipe
|
||||
sudo ./felhom-wipe.sh --level full --yes
|
||||
```
|
||||
|
||||
### Wipe levels
|
||||
|
||||
| Level | What it removes |
|
||||
|-------|-----------------|
|
||||
| `soft` | Controller state files only: `settings.json`, `metrics.db`, `setup-state.json`, `update-state.json`, `session-data.json`, `snapshot-history.json` |
|
||||
| `controller` | Soft + all non-infra Docker containers, all Docker volumes (except `portainer_data`), all stack directories (skips protected stacks by default) |
|
||||
| `full` | Controller + `felhom-data/` on all storage drives (appdata, backups). Also removes old-style `appdata/` and `backups/` directories for pre-v0.26.0 compatibility. Restarts controller after cleanup. |
|
||||
| `nuclear` | Full + `controller.yaml`, all infra containers (controller, traefik, cloudflared, portainer), DR markers (`.felhom-infra-backup/` on all drives), `docker system prune -af --volumes` |
|
||||
|
||||
### CLI options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--level <level>` | Required. One of: `soft`, `controller`, `full`, `nuclear` |
|
||||
| `--yes` | Execute the wipe. Default is dry-run (preview only). |
|
||||
| `--include-protected` | Also remove protected stacks (controller level only). |
|
||||
|
||||
### Path auto-detection
|
||||
|
||||
- Reads `stacks_dir` and `data_dir` from `/opt/docker/felhom-controller/controller.yaml` if present
|
||||
- Reads registered storage paths from `settings.json`
|
||||
- Also scans `/mnt/*/` for `felhom-data/` or legacy `appdata/` directories not in the registry
|
||||
|
||||
### What is preserved
|
||||
|
||||
- OS and system files
|
||||
- Infrastructure containers and config (unless `nuclear`)
|
||||
- User files: `Dokumentumok/`, `media/`, other non-felhom directories on drives
|
||||
- DR markers on drives (unless `nuclear`)
|
||||
|
||||
### Safety
|
||||
|
||||
- Dry-run by default — shows plan without deleting anything
|
||||
- Interactive `YES` confirmation prompt required even with `--yes`
|
||||
- Must run as root (`sudo`)
|
||||
- Checks Docker is running before proceeding
|
||||
- Protected stacks skipped by default (use `--include-protected` to override)
|
||||
|
||||
### Redeploy after nuclear wipe
|
||||
|
||||
```bash
|
||||
curl -fsSL https://gitea.dooplex.hu/admin/deploy-felhom-compose/raw/branch/main/scripts/docker-setup.sh | bash
|
||||
```
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ===================================================================
|
||||
# felhom-wipe.sh — Clean felhom data from a test node
|
||||
# Usage: ./felhom-wipe.sh --level <soft|controller|full|nuclear> [--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 <<EOF
|
||||
Usage: $(basename "$0") --level <level> [--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 present
|
||||
if [ -f "$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
|
||||
}
|
||||
|
||||
# --- 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-data dirs not in registry
|
||||
for d in /mnt/*/; do
|
||||
[ -d "${d}felhom-data" ] || [ -d "${d}appdata" ] || 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}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 all drives)"
|
||||
echo -e " ${RED}DELETE${NC} All Docker data (docker system prune -af --volumes)"
|
||||
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 ""
|
||||
}
|
||||
|
||||
# --- 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
|
||||
[ -f "$f" ] && rm -f "$f" && info " Removed: $f"
|
||||
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
|
||||
|
||||
# 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 controller.yaml
|
||||
[ -f "$CONTROLLER_YAML" ] && rm -f "$CONTROLLER_YAML" && info " Removed: controller.yaml"
|
||||
|
||||
# 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
|
||||
|
||||
# Remove all Docker data
|
||||
warn "Pruning all Docker data..."
|
||||
docker system prune -af --volumes 2>/dev/null || warn "Docker prune failed"
|
||||
|
||||
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 "$@"
|
||||
Reference in New Issue
Block a user