diff --git a/scripts/README.md b/scripts/README.md index bef210c..59ac75f 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -28,10 +28,8 @@ sudo ./docker-setup.sh \ --cf-token "your-cloudflare-api-token" \ --customer "customer-1" -# Hub mode — download pre-configured controller.yaml from Felhom Hub +# Hub mode — one-liner: all infra settings (domain, email, CF tokens) come from Hub sudo ./docker-setup.sh \ - --domain example.com \ - --email admin@example.com \ --hub-customer "customer-1" \ --hub-password "retrieval-password-from-hub" ``` @@ -94,26 +92,33 @@ The script supports three mutually exclusive TLS modes: - Generates 10-year wildcard cert with custom CA - CA cert copied to user home for manual device import -### Hub download mode +### Hub mode When both `--hub-customer` and `--hub-password` are provided, the script downloads a -pre-configured `controller.yaml` from the Felhom Hub instead of running the interactive wizard: +pre-configured `controller.yaml` from the Felhom Hub **before any infra setup begins**, +then extracts the stored values to auto-configure everything — no additional flags needed: ``` GET https://hub.felhom.eu/api/v1/config/{customer_id} Header: X-Retrieval-Password: {password} ``` -On success: -- Saves downloaded YAML as `controller.yaml` (permissions 600) -- Extracts domain, email, CF tokens for use by subsequent setup steps (Traefik, Cloudflare Tunnel) -- Skips the interactive wizard entirely +The downloaded config is parsed early in the run and populates: -On failure: -- Logs a warning with HTTP status code -- Falls back to the interactive wizard +| Extracted field | Used for | +|-----------------|----------| +| `customer.domain` | Traefik routing, TLS cert SANs, DNS display | +| `customer.email` | Let's Encrypt ACME registration | +| `infrastructure.cf_api_token` | Traefik DNS-01 TLS challenge | +| `infrastructure.cf_tunnel_token` | Cloudflare Tunnel connector | -Hub credentials are created in the Hub web UI at `https://hub.felhom.eu/configs`. +CLI flags always take precedence — passing `--domain` overrides the hub value. + +On failure (wrong credentials, network error): +- Script exits immediately with the HTTP status code and the failing URL +- Nothing is installed + +Hub credentials are found in the Hub web UI under the customer's **Credentials** section. ### Configuration wizard @@ -220,8 +225,8 @@ sudo ./felhom-wipe.sh --level full --yes |-------|-----------------| | `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`-level cleanup + `felhom-data/` on all storage drives (appdata, backups). Also removes old-style `appdata/` and `backups/` directories for pre-v0.26.0 compatibility. Infra containers (including felhom-controller) are **preserved**; controller is restarted after cleanup. | -| `nuclear` | Full + all infra containers (controller, traefik, cloudflared, portainer), DR markers (`.felhom-infra-backup/` on all drives), `docker system prune -af --volumes`, and all infra config directories (`/opt/docker/felhom-controller/`, `/opt/docker/traefik/`, `/opt/docker/cloudflared/`, `/opt/docker/stacks/`) | +| `full` | `controller`-level cleanup + `felhom-data/` on all storage drives (appdata, backups). Also removes old-style `appdata/` and `backups/` directories for pre-v0.26.0 compatibility. Removes `/mnt/.felhom-scan/` (stale DR scan dir). Infra containers (including felhom-controller) are **preserved**; controller is restarted after cleanup. | +| `nuclear` | Full + all infra containers (controller, traefik, cloudflared, portainer), DR markers (`.felhom-infra-backup/` on all drives), raw helper mounts (`/mnt/.felhom-raw/` — unmount bind+raw, strip fstab entries), `/mnt/.felhom-scan/`, `docker system prune -af --volumes`, and all infra config directories (`/opt/docker/felhom-controller/`, `/opt/docker/traefik/`, `/opt/docker/cloudflared/`, `/opt/docker/stacks/`) | ### CLI options @@ -237,11 +242,35 @@ sudo ./felhom-wipe.sh --level full --yes - Reads registered storage paths from `settings.json` - Also scans `/mnt/*/` for `felhom-data/` or legacy `appdata/` directories not in the registry +### Raw helper mounts + +The attach wizard creates a two-level mount structure for pre-formatted drives: + +``` +/dev/sdb1 (physical partition) + └─ /mnt/.felhom-raw/hdd_1/ ← raw mount (persists in fstab, backs the bind) + └─ felhom_data/ + └─ /mnt/hdd_1/ ← bind mount (what apps actually use) +``` + +Both `fstab` entries survive reboots. On `nuclear` wipe, the script: +1. Unmounts bind mounts (e.g. `/mnt/hdd_1`) first +2. Unmounts raw mounts (e.g. `/mnt/.felhom-raw/hdd_1`) +3. Strips both `fstab` entries +4. Removes the now-empty `/mnt/.felhom-raw/` directory + +The physical data on the drive partition is **not touched** — only the mount point +directories (empty after unmounting) are removed. + +`/mnt/.felhom-scan/` is a separate ephemeral directory used only during the DR setup +wizard to temporarily inspect drives. It is cleaned up from `full` level onwards. + ### What is preserved - OS and system files - Infrastructure containers and config (unless `nuclear`) - User files: `Dokumentumok/`, `media/`, other non-felhom directories on drives +- Drive data — raw mounts are unmounted but partition contents are untouched - DR markers on drives (unless `nuclear`) ### Safety diff --git a/scripts/docker-setup.sh b/scripts/docker-setup.sh index 33dd305..ba87de9 100644 --- a/scripts/docker-setup.sh +++ b/scripts/docker-setup.sh @@ -144,6 +144,11 @@ CUSTOMER_ID="" CF_TUNNEL_TOKEN="" HUB_CUSTOMER="" HUB_PASSWORD="" +HUB_CONFIG_TMP="" # path to downloaded hub config temp file (set by apply_hub_config) +DOMAIN_FROM_CLI=false +EMAIL_FROM_CLI=false +CF_TOKEN_FROM_CLI=false +CF_TUNNEL_FROM_CLI=false # Directories DOCKER_DATA_DIR="/opt/docker" @@ -302,13 +307,13 @@ parse_args() { INTERFACE="$2"; shift 2 ;; --domain) require_arg "$1" "${2:-}" - BASE_DOMAIN="$2"; shift 2 ;; + BASE_DOMAIN="$2"; DOMAIN_FROM_CLI=true; shift 2 ;; --email) require_arg "$1" "${2:-}" - ACME_EMAIL="$2"; shift 2 ;; + ACME_EMAIL="$2"; EMAIL_FROM_CLI=true; shift 2 ;; --cf-token) require_arg "$1" "${2:-}" - CF_DNS_API_TOKEN="$2"; shift 2 ;; + CF_DNS_API_TOKEN="$2"; CF_TOKEN_FROM_CLI=true; shift 2 ;; --traefik-password) require_arg "$1" "${2:-}" TRAEFIK_PASSWORD="$2"; shift 2 ;; @@ -317,7 +322,7 @@ parse_args() { CUSTOMER_ID="$2"; shift 2 ;; --cf-tunnel-token) require_arg "$1" "${2:-}" - CF_TUNNEL_TOKEN="$2"; shift 2 ;; + CF_TUNNEL_TOKEN="$2"; CF_TUNNEL_FROM_CLI=true; shift 2 ;; --hub-customer) require_arg "$1" "${2:-}" HUB_CUSTOMER="$2"; shift 2 ;; @@ -1461,6 +1466,88 @@ EOF } +#------------------------------------------------------------------------------- +# YAML helper: extract a single string value from a section+key +# Usage: yaml_get +# Handles both quoted ("value") and unquoted values. +#------------------------------------------------------------------------------- +yaml_get() { + local file="$1" section="$2" key="$3" + awk -v s="${section}:" -v k=" ${key}:" ' + /^[[:alpha:]]/ { in_s = ($0 == s) } + in_s && index($0, k) == 1 { + sub(/^[^:]*: */, ""); gsub(/^"|"$/, ""); print; exit + } + ' "$file" +} + +#------------------------------------------------------------------------------- +# Hub mode: download controller.yaml early and extract infra vars +# Called from main() before Traefik/infra setup so BASE_DOMAIN etc. are ready. +#------------------------------------------------------------------------------- +apply_hub_config() { + [[ -z "$HUB_CUSTOMER" ]] && return + + log_info "Fetching configuration from Felhom Hub (customer: ${HUB_CUSTOMER})..." + + if [[ "$DRY_RUN" == true ]]; then + echo -e "${CYAN}[DRY-RUN]${NC} Would fetch: https://hub.felhom.eu/api/v1/config/${HUB_CUSTOMER}" + echo -e "${CYAN}[DRY-RUN]${NC} Would apply domain, email, CF tokens from hub config" + # Set plausible placeholders so the plan display is meaningful + [[ "$DOMAIN_FROM_CLI" == false ]] && BASE_DOMAIN="" + [[ "$EMAIL_FROM_CLI" == false ]] && ACME_EMAIL="" + [[ "$CF_TOKEN_FROM_CLI" == false ]] && CF_DNS_API_TOKEN="" + [[ "$CF_TUNNEL_FROM_CLI" == false ]] && CF_TUNNEL_TOKEN="" + return + fi + + HUB_CONFIG_TMP=$(mktemp /tmp/felhom-hub-config-XXXXXX.yaml) + + local hub_url="https://hub.felhom.eu/api/v1/config/${HUB_CUSTOMER}" + local http_code + http_code=$(curl -fsSL \ + -H "X-Retrieval-Password: ${HUB_PASSWORD}" \ + -o "${HUB_CONFIG_TMP}" \ + -w "%{http_code}" \ + "${hub_url}" 2>&1) || true + + if [[ "$http_code" != "200" ]]; then + rm -f "${HUB_CONFIG_TMP}" + HUB_CONFIG_TMP="" + log_error "Failed to fetch config from Felhom Hub (HTTP ${http_code})" + log_error "URL: ${hub_url}" + log_error "Check the customer ID and retrieval password, then re-run." + exit 1 + fi + + log_success "Hub config fetched successfully" + + # Extract values from hub YAML + local hub_domain hub_email hub_cf_token hub_tunnel_token + hub_domain=$(yaml_get "${HUB_CONFIG_TMP}" "customer" "domain") + hub_email=$(yaml_get "${HUB_CONFIG_TMP}" "customer" "email") + hub_cf_token=$(yaml_get "${HUB_CONFIG_TMP}" "infrastructure" "cf_api_token") + hub_tunnel_token=$(yaml_get "${HUB_CONFIG_TMP}" "infrastructure" "cf_tunnel_token") + + # Apply to script vars — CLI flags always take precedence + if [[ "$DOMAIN_FROM_CLI" == false && -n "$hub_domain" ]]; then + BASE_DOMAIN="$hub_domain" + log_info " domain: ${BASE_DOMAIN} (from Hub)" + fi + if [[ "$EMAIL_FROM_CLI" == false && -n "$hub_email" ]]; then + ACME_EMAIL="$hub_email" + log_info " email: ${ACME_EMAIL} (from Hub)" + fi + if [[ "$CF_TOKEN_FROM_CLI" == false && -n "$hub_cf_token" ]]; then + CF_DNS_API_TOKEN="$hub_cf_token" + log_info " cf_api_token: ${CF_DNS_API_TOKEN:0:6}... (from Hub)" + fi + if [[ "$CF_TUNNEL_FROM_CLI" == false && -n "$hub_tunnel_token" ]]; then + CF_TUNNEL_TOKEN="$hub_tunnel_token" + log_info " cf_tunnel_token: ${CF_TUNNEL_TOKEN:0:6}... (from Hub)" + fi +} + #------------------------------------------------------------------------------- # Generate minimal controller.yaml — full configuration via web UI setup wizard #------------------------------------------------------------------------------- @@ -1474,30 +1561,24 @@ generate_minimal_config() { mkdir -p "${CONTROLLER_DIR}" if [[ -n "$HUB_CUSTOMER" ]]; then - log_step "${step_num}/$(get_total_steps) - Downloading controller.yaml from Felhom Hub..." + log_step "${step_num}/$(get_total_steps) - Installing controller.yaml from Felhom Hub..." if [[ "$DRY_RUN" == true ]]; then - echo -e "${CYAN}[DRY-RUN]${NC} Would download controller.yaml from https://hub.felhom.eu/api/v1/config/${HUB_CUSTOMER}" + echo -e "${CYAN}[DRY-RUN]${NC} Would install hub controller.yaml to ${CONTROLLER_DIR}/controller.yaml" return fi - local hub_url="https://hub.felhom.eu/api/v1/config/${HUB_CUSTOMER}" - local http_code - http_code=$(curl -fsSL \ - -H "X-Retrieval-Password: ${HUB_PASSWORD}" \ - -o "${CONTROLLER_DIR}/controller.yaml" \ - -w "%{http_code}" \ - "${hub_url}" 2>&1) || true - - if [[ "$http_code" == "200" ]]; then - chmod 600 "${CONTROLLER_DIR}/controller.yaml" - log_success "controller.yaml downloaded from Felhom Hub (customer: ${HUB_CUSTOMER})" + # Config was already downloaded by apply_hub_config() early in main() + if [[ -n "$HUB_CONFIG_TMP" && -f "$HUB_CONFIG_TMP" ]]; then + mv "${HUB_CONFIG_TMP}" "${CONTROLLER_DIR}/controller.yaml" + HUB_CONFIG_TMP="" else - log_error "Failed to download controller.yaml from Hub (HTTP ${http_code})" - log_error "URL: ${hub_url}" - log_error "Check the customer ID and retrieval password, then re-run." + log_error "Hub config temp file not found — apply_hub_config() may not have run" exit 1 fi + + chmod 600 "${CONTROLLER_DIR}/controller.yaml" + log_success "controller.yaml installed from Felhom Hub (customer: ${HUB_CUSTOMER})" return fi @@ -1734,7 +1815,11 @@ main() { if [[ "$DEBUG_MODE" == true ]]; then set -x fi - + + # Hub mode: download config early so BASE_DOMAIN, ACME_EMAIL, CF tokens are + # available before Traefik and other infra steps run + apply_hub_config + print_banner check_debian diff --git a/scripts/felhom-wipe.sh b/scripts/felhom-wipe.sh index 9238f46..24ebbbe 100644 --- a/scripts/felhom-wipe.sh +++ b/scripts/felhom-wipe.sh @@ -209,6 +209,16 @@ print_plan() { 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}" @@ -239,6 +249,21 @@ print_plan() { 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)" + 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${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") @@ -264,6 +289,57 @@ print_plan() { 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) + if [ -f /etc/fstab ]; then + local bind_targets + 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 directory — safe now that mounts are gone + rm -rf /mnt/.felhom-raw && info " Removed: /mnt/.felhom-raw/" +} + # --- Wipe Functions --- do_soft_wipe() { @@ -336,6 +412,9 @@ do_full_wipe() { 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" @@ -358,6 +437,10 @@ do_nuclear_wipe() { 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"