feat: add version checking and install manifest tracking
Add minimum version validation for all dependencies across local and remote machines (jq>=1.6, curl>=7.70, git>=2.30, docker>=20.0, compose>=2.0, shellcheck>=0.8, gh>=2.0). Setup scripts now record every install action to .manifests/<host>.manifest files, enabling full rollback via setup/cleanup.sh. teardown_all.sh gains --cleanup flag to chain prerequisite removal after phase teardowns. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,6 +12,9 @@ runners.conf
|
|||||||
/tmp/
|
/tmp/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Install manifests (machine-specific state from setup scripts)
|
||||||
|
.manifests/
|
||||||
|
|
||||||
# Backup archives
|
# Backup archives
|
||||||
*.zip
|
*.zip
|
||||||
|
|
||||||
|
|||||||
14
CLAUDE.md
14
CLAUDE.md
@@ -35,12 +35,24 @@ backup/ # Backup and restore scripts
|
|||||||
|
|
||||||
## Key Commands
|
## Key Commands
|
||||||
- `setup/configure_env.sh` — Interactive .env setup wizard
|
- `setup/configure_env.sh` — Interactive .env setup wizard
|
||||||
|
- `setup/cleanup.sh` — Reverse everything setup scripts installed (reads .manifests/)
|
||||||
- `preflight.sh` — Validate everything before running phases
|
- `preflight.sh` — Validate everything before running phases
|
||||||
- `run_all.sh` — Execute all phases sequentially
|
- `run_all.sh` — Execute all phases sequentially
|
||||||
- `teardown_all.sh` — Reverse teardown
|
- `teardown_all.sh` — Reverse teardown (add `--cleanup` to also uninstall prerequisites)
|
||||||
- `manage_runner.sh add|remove|list` — Dynamic runner management
|
- `manage_runner.sh add|remove|list` — Dynamic runner management
|
||||||
|
|
||||||
|
## Version Checking
|
||||||
|
Setup scripts and preflight validate minimum versions for all tools:
|
||||||
|
- Local: jq>=1.6, curl>=7.70, git>=2.30, shellcheck>=0.8, gh>=2.0
|
||||||
|
- Remote: docker>=20.0, docker-compose>=2.0, jq>=1.6
|
||||||
|
|
||||||
|
## Install Manifests
|
||||||
|
Setup scripts record every install action to `.manifests/<host>.manifest`.
|
||||||
|
`setup/cleanup.sh` reads these manifests to fully reverse setup actions.
|
||||||
|
Useful for cleaning machines after testing or migrating to new servers.
|
||||||
|
|
||||||
## Sensitive Files (never commit)
|
## Sensitive Files (never commit)
|
||||||
- `.env` — contains passwords, tokens, IPs
|
- `.env` — contains passwords, tokens, IPs
|
||||||
- `runners.conf` — contains server IPs and paths
|
- `runners.conf` — contains server IPs and paths
|
||||||
|
- `.manifests/` — machine-specific install state
|
||||||
- `*.pem`, `*.key`, `*.crt` — SSL certificates
|
- `*.pem`, `*.key`, `*.crt` — SSL certificates
|
||||||
|
|||||||
175
lib/common.sh
175
lib/common.sh
@@ -352,3 +352,178 @@ wait_for_ssh() {
|
|||||||
log_error "Timeout waiting for SSH to ${host_key} after ${max_secs}s"
|
log_error "Timeout waiting for SSH to ${host_key} after ${max_secs}s"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Version checking
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Compare two semver-like version strings (major.minor or major.minor.patch).
|
||||||
|
# Returns 0 if $1 >= $2, 1 otherwise.
|
||||||
|
# Works by comparing each numeric component left to right.
|
||||||
|
_version_gte() {
|
||||||
|
local ver="$1" min="$2"
|
||||||
|
|
||||||
|
# Split on dots into arrays
|
||||||
|
local IFS='.'
|
||||||
|
# shellcheck disable=SC2206
|
||||||
|
local -a v=($ver)
|
||||||
|
# shellcheck disable=SC2206
|
||||||
|
local -a m=($min)
|
||||||
|
|
||||||
|
local max_parts=${#m[@]}
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < max_parts; i++)); do
|
||||||
|
local vp="${v[$i]:-0}"
|
||||||
|
local mp="${m[$i]:-0}"
|
||||||
|
if (( vp > mp )); then return 0; fi
|
||||||
|
if (( vp < mp )); then return 1; fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the first semver-like string (X.Y or X.Y.Z) from arbitrary output.
|
||||||
|
# Handles common patterns like "jq-1.7.1", "Docker version 24.0.7", "v2.29.1", etc.
|
||||||
|
_extract_version() {
|
||||||
|
local raw="$1"
|
||||||
|
# Match the first occurrence of digits.digits (optionally .digits more)
|
||||||
|
if [[ "$raw" =~ ([0-9]+\.[0-9]+(\.[0-9]+)*) ]]; then
|
||||||
|
printf '%s' "${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
printf ''
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check that a local command meets a minimum version.
|
||||||
|
# Usage: check_min_version "docker" "docker --version" "20.0"
|
||||||
|
# tool_name: display name for log messages
|
||||||
|
# version_cmd: command to run (must output version somewhere in its output)
|
||||||
|
# min_version: minimum required version (e.g. "1.6", "20.0.0")
|
||||||
|
# Returns 0 if version >= min, 1 otherwise.
|
||||||
|
check_min_version() {
|
||||||
|
local tool_name="$1" version_cmd="$2" min_version="$3"
|
||||||
|
local raw_output
|
||||||
|
raw_output=$(eval "$version_cmd" 2>&1) || {
|
||||||
|
log_error "$tool_name: failed to run '$version_cmd'"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
local actual
|
||||||
|
actual=$(_extract_version "$raw_output")
|
||||||
|
if [[ -z "$actual" ]]; then
|
||||||
|
log_error "$tool_name: could not parse version from: $raw_output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if _version_gte "$actual" "$min_version"; then
|
||||||
|
log_success "$tool_name $actual (>= $min_version)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "$tool_name $actual is below minimum $min_version"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Same as check_min_version but runs the command on a remote host via SSH.
|
||||||
|
# Usage: check_remote_min_version "UNRAID" "docker" "docker --version" "20.0"
|
||||||
|
check_remote_min_version() {
|
||||||
|
local host_key="$1" tool_name="$2" version_cmd="$3" min_version="$4"
|
||||||
|
local raw_output
|
||||||
|
raw_output=$(ssh_exec "$host_key" "$version_cmd" 2>&1) || {
|
||||||
|
log_error "$tool_name on ${host_key}: failed to run '$version_cmd'"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
local actual
|
||||||
|
actual=$(_extract_version "$raw_output")
|
||||||
|
if [[ -z "$actual" ]]; then
|
||||||
|
log_error "$tool_name on ${host_key}: could not parse version from: $raw_output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if _version_gte "$actual" "$min_version"; then
|
||||||
|
log_success "$tool_name $actual on ${host_key} (>= $min_version)"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
log_error "$tool_name $actual on ${host_key} is below minimum $min_version"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Install manifest — tracks what each setup script installs for rollback
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Manifest files live in $PROJECT_ROOT/.manifests/<host>.manifest
|
||||||
|
# Each line: TYPE|TARGET|DETAILS
|
||||||
|
# Types:
|
||||||
|
# brew_pkg — Homebrew package on macOS (TARGET=package_name)
|
||||||
|
# dnf_pkg — DNF package on Fedora (TARGET=package_name)
|
||||||
|
# static_bin — Static binary installed to a path (TARGET=path)
|
||||||
|
# docker_group — User added to docker group (TARGET=username)
|
||||||
|
# systemd_svc — Systemd service enabled (TARGET=service_name)
|
||||||
|
# xcode_cli — Xcode CLI Tools installed (TARGET=xcode-select)
|
||||||
|
# directory — Directory created (TARGET=path)
|
||||||
|
|
||||||
|
_manifest_dir() {
|
||||||
|
printf '%s/.manifests' "$(_project_root)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize manifest directory and file for a host.
|
||||||
|
# Usage: manifest_init "macbook"
|
||||||
|
manifest_init() {
|
||||||
|
local host="$1"
|
||||||
|
local dir
|
||||||
|
dir="$(_manifest_dir)"
|
||||||
|
mkdir -p "$dir"
|
||||||
|
local file="${dir}/${host}.manifest"
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
printf '# Install manifest for %s — created %s\n' "$host" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Record an action in the manifest. Skips duplicates.
|
||||||
|
# Usage: manifest_record "macbook" "brew_pkg" "jq"
|
||||||
|
# manifest_record "unraid" "static_bin" "/usr/local/bin/jq"
|
||||||
|
manifest_record() {
|
||||||
|
local host="$1" action_type="$2" target="$3" details="${4:-}"
|
||||||
|
local file
|
||||||
|
file="$(_manifest_dir)/${host}.manifest"
|
||||||
|
|
||||||
|
# Ensure manifest file exists
|
||||||
|
manifest_init "$host"
|
||||||
|
|
||||||
|
local entry="${action_type}|${target}|${details}"
|
||||||
|
|
||||||
|
# Skip if already recorded
|
||||||
|
if grep -qF "$entry" "$file" 2>/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$entry" >> "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a manifest file exists and has entries (beyond the header).
|
||||||
|
# Usage: manifest_exists "macbook"
|
||||||
|
manifest_exists() {
|
||||||
|
local host="$1"
|
||||||
|
local file
|
||||||
|
file="$(_manifest_dir)/${host}.manifest"
|
||||||
|
[[ -f "$file" ]] && grep -qv '^#' "$file" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read all entries from a manifest (skipping comments).
|
||||||
|
# Usage: manifest_entries "macbook"
|
||||||
|
# Outputs lines to stdout: TYPE|TARGET|DETAILS
|
||||||
|
manifest_entries() {
|
||||||
|
local host="$1"
|
||||||
|
local file
|
||||||
|
file="$(_manifest_dir)/${host}.manifest"
|
||||||
|
if [[ ! -f "$file" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
grep -v '^#' "$file" | grep -v '^$' || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the manifest file for a host (after successful cleanup).
|
||||||
|
# Usage: manifest_clear "macbook"
|
||||||
|
manifest_clear() {
|
||||||
|
local host="$1"
|
||||||
|
local file
|
||||||
|
file="$(_manifest_dir)/${host}.manifest"
|
||||||
|
rm -f "$file"
|
||||||
|
}
|
||||||
|
|||||||
42
preflight.sh
42
preflight.sh
@@ -16,7 +16,7 @@ FAIL_COUNT=0
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Check helper — runs a check function, tracks pass/fail count.
|
# Check helper — runs a check function, tracks pass/fail count.
|
||||||
# Intentionally does NOT exit on failure — we want to run ALL 19 checks
|
# Intentionally does NOT exit on failure — we want to run ALL checks
|
||||||
# so the user sees every issue at once, not one at a time.
|
# so the user sees every issue at once, not one at a time.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
check() {
|
check() {
|
||||||
@@ -293,11 +293,49 @@ if ! check_nginx_conf 2>/dev/null; then
|
|||||||
log_error " → Nginx config path ${NGINX_CONF_PATH:-} not writable on Unraid."
|
log_error " → Nginx config path ${NGINX_CONF_PATH:-} not writable on Unraid."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Check 20: Local tool minimum versions
|
||||||
|
# Validates that tools on the MacBook meet minimum requirements.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_local_versions() {
|
||||||
|
local fail=0
|
||||||
|
check_min_version "jq" "jq --version" "1.6" || fail=1
|
||||||
|
check_min_version "curl" "curl --version" "7.70" || fail=1
|
||||||
|
check_min_version "git" "git --version" "2.30" || fail=1
|
||||||
|
return $fail
|
||||||
|
}
|
||||||
|
check 20 "Local tool minimum versions (jq>=1.6, curl>=7.70, git>=2.30)" check_local_versions
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Check 21: Unraid tool minimum versions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_unraid_versions() {
|
||||||
|
local fail=0
|
||||||
|
check_remote_min_version "UNRAID" "docker" "docker --version" "20.0" || fail=1
|
||||||
|
check_remote_min_version "UNRAID" "docker-compose" "docker compose version 2>/dev/null || docker-compose --version" "2.0" || fail=1
|
||||||
|
check_remote_min_version "UNRAID" "jq" "jq --version" "1.6" || fail=1
|
||||||
|
return $fail
|
||||||
|
}
|
||||||
|
check 21 "Unraid tool minimum versions (docker>=20, compose>=2, jq>=1.6)" check_unraid_versions
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Check 22: Fedora tool minimum versions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_fedora_versions() {
|
||||||
|
local fail=0
|
||||||
|
check_remote_min_version "FEDORA" "docker" "docker --version" "20.0" || fail=1
|
||||||
|
check_remote_min_version "FEDORA" "docker-compose" "docker compose version" "2.0" || fail=1
|
||||||
|
check_remote_min_version "FEDORA" "jq" "jq --version" "1.6" || fail=1
|
||||||
|
return $fail
|
||||||
|
}
|
||||||
|
check 22 "Fedora tool minimum versions (docker>=20, compose>=2, jq>=1.6)" check_fedora_versions
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Summary
|
# Summary
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
TOTAL_CHECKS=$((PASS_COUNT + FAIL_COUNT))
|
||||||
printf '\n'
|
printf '\n'
|
||||||
log_info "Results: ${PASS_COUNT} passed, ${FAIL_COUNT} failed (out of 19 checks)"
|
log_info "Results: ${PASS_COUNT} passed, ${FAIL_COUNT} failed (out of ${TOTAL_CHECKS} checks)"
|
||||||
|
|
||||||
if [[ $FAIL_COUNT -gt 0 ]]; then
|
if [[ $FAIL_COUNT -gt 0 ]]; then
|
||||||
log_error "Preflight FAILED — fix the issues above before proceeding."
|
log_error "Preflight FAILED — fix the issues above before proceeding."
|
||||||
|
|||||||
299
setup/cleanup.sh
Executable file
299
setup/cleanup.sh
Executable file
@@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# setup/cleanup.sh — Reverse everything installed by setup scripts
|
||||||
|
# Reads .manifests/<host>.manifest files and undoes each recorded action.
|
||||||
|
# Use this to clean up machines after testing or after migrating to new servers.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./setup/cleanup.sh # Clean up all hosts with manifests
|
||||||
|
# ./setup/cleanup.sh --host=macbook # Clean up macOS only
|
||||||
|
# ./setup/cleanup.sh --host=unraid # Clean up Unraid only
|
||||||
|
# ./setup/cleanup.sh --host=fedora # Clean up Fedora only
|
||||||
|
# ./setup/cleanup.sh --dry-run # Show what would be removed
|
||||||
|
# ./setup/cleanup.sh --yes # Skip confirmation prompts
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||||
|
|
||||||
|
load_env || true # Best effort — .env may already be gone during full cleanup
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parse arguments
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
TARGET_HOST=""
|
||||||
|
DRY_RUN=false
|
||||||
|
AUTO_YES=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--host=*) TARGET_HOST="${arg#*=}" ;;
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
--yes|-y) AUTO_YES=true ;;
|
||||||
|
--help|-h)
|
||||||
|
cat <<EOF
|
||||||
|
Usage: $(basename "$0") [options]
|
||||||
|
|
||||||
|
Reverses actions recorded in .manifests/ by setup scripts.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--host=NAME Clean up a specific host: macbook, unraid, fedora
|
||||||
|
--dry-run Show what would be removed without doing it
|
||||||
|
--yes, -y Skip confirmation prompts
|
||||||
|
--help Show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
$(basename "$0") Clean up all hosts
|
||||||
|
$(basename "$0") --host=fedora Clean up Fedora only
|
||||||
|
$(basename "$0") --dry-run Preview cleanup actions
|
||||||
|
EOF
|
||||||
|
exit 0 ;;
|
||||||
|
*) log_error "Unknown argument: $arg"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Discover which hosts have manifests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
MANIFEST_DIR="$(_project_root)/.manifests"
|
||||||
|
|
||||||
|
if [[ ! -d "$MANIFEST_DIR" ]]; then
|
||||||
|
log_info "No .manifests/ directory found — nothing to clean up."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
HOSTS=()
|
||||||
|
if [[ -n "$TARGET_HOST" ]]; then
|
||||||
|
if [[ ! -f "${MANIFEST_DIR}/${TARGET_HOST}.manifest" ]]; then
|
||||||
|
log_error "No manifest found for host '${TARGET_HOST}'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
HOSTS=("$TARGET_HOST")
|
||||||
|
else
|
||||||
|
for f in "${MANIFEST_DIR}"/*.manifest; do
|
||||||
|
[[ -f "$f" ]] || continue
|
||||||
|
host=$(basename "$f" .manifest)
|
||||||
|
HOSTS+=("$host")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#HOSTS[@]} -eq 0 ]]; then
|
||||||
|
log_info "No manifest files found — nothing to clean up."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "Hosts with install manifests: ${HOSTS[*]}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Confirmation (unless --yes or --dry-run)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [[ "$DRY_RUN" == "false" ]] && [[ "$AUTO_YES" == "false" ]]; then
|
||||||
|
log_warn "This will attempt to uninstall everything recorded in the manifests."
|
||||||
|
printf 'Continue? [y/N] '
|
||||||
|
read -r confirm
|
||||||
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
||||||
|
log_info "Cleanup cancelled"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cleanup functions — one per action type
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
cleanup_brew_pkg() {
|
||||||
|
local pkg="$1"
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
log_info "[dry-run] Would uninstall brew package: $pkg"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if brew list "$pkg" &>/dev/null; then
|
||||||
|
log_info "Uninstalling brew package: $pkg"
|
||||||
|
brew uninstall "$pkg" || log_warn "Failed to uninstall $pkg (may have dependents)"
|
||||||
|
else
|
||||||
|
log_info "Brew package $pkg not installed — skipping"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_dnf_pkg() {
|
||||||
|
local host_key="$1" pkg="$2"
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
log_info "[dry-run] Would remove dnf package on ${host_key}: $pkg"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log_info "Removing dnf package on ${host_key}: $pkg"
|
||||||
|
ssh_exec "$host_key" "sudo dnf -y remove $pkg" 2>/dev/null || log_warn "Failed to remove $pkg on ${host_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_static_bin() {
|
||||||
|
local host_key="$1" path="$2"
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
log_info "[dry-run] Would remove static binary on ${host_key}: $path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log_info "Removing static binary on ${host_key}: $path"
|
||||||
|
ssh_exec "$host_key" "rm -f '$path'" 2>/dev/null || log_warn "Failed to remove $path on ${host_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_docker_group() {
|
||||||
|
local host_key="$1" username="$2"
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
log_info "[dry-run] Would remove ${username} from docker group on ${host_key}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log_info "Removing ${username} from docker group on ${host_key}"
|
||||||
|
ssh_exec "$host_key" "sudo gpasswd -d $username docker" 2>/dev/null || log_warn "Failed to remove $username from docker group on ${host_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_systemd_svc() {
|
||||||
|
local host_key="$1" service="$2"
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
log_info "[dry-run] Would disable systemd service on ${host_key}: $service"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log_info "Disabling systemd service on ${host_key}: $service"
|
||||||
|
ssh_exec "$host_key" "sudo systemctl disable --now $service" 2>/dev/null || log_warn "Failed to disable $service on ${host_key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_xcode_cli() {
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
log_info "[dry-run] Would remove Xcode CLI Tools"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
log_info "Removing Xcode Command Line Tools..."
|
||||||
|
sudo rm -rf /Library/Developer/CommandLineTools 2>/dev/null || log_warn "Failed to remove Xcode CLI Tools (may need sudo)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Map host names to SSH host keys for remote operations
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
host_to_ssh_key() {
|
||||||
|
local host="$1"
|
||||||
|
case "$host" in
|
||||||
|
unraid) echo "UNRAID" ;;
|
||||||
|
fedora) echo "FEDORA" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Process each host's manifest in reverse order
|
||||||
|
# Reverse order ensures dependent packages are removed before their providers
|
||||||
|
# (e.g., docker-compose-plugin before docker-ce).
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
TOTAL_ACTIONS=0
|
||||||
|
CLEANED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for host in "${HOSTS[@]}"; do
|
||||||
|
log_info "=== Cleaning up: ${host} ==="
|
||||||
|
|
||||||
|
ssh_key=$(host_to_ssh_key "$host")
|
||||||
|
|
||||||
|
# Read entries into array, then reverse
|
||||||
|
mapfile -t entries < <(manifest_entries "$host")
|
||||||
|
|
||||||
|
if [[ ${#entries[@]} -eq 0 ]]; then
|
||||||
|
log_info "No entries in ${host} manifest — skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Process in reverse order
|
||||||
|
for ((i = ${#entries[@]} - 1; i >= 0; i--)); do
|
||||||
|
entry="${entries[$i]}"
|
||||||
|
IFS='|' read -r action_type target _details <<< "$entry"
|
||||||
|
TOTAL_ACTIONS=$((TOTAL_ACTIONS + 1))
|
||||||
|
|
||||||
|
case "$action_type" in
|
||||||
|
brew_pkg)
|
||||||
|
if cleanup_brew_pkg "$target"; then
|
||||||
|
CLEANED=$((CLEANED + 1))
|
||||||
|
else
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dnf_pkg)
|
||||||
|
if [[ -z "$ssh_key" ]]; then
|
||||||
|
log_warn "Cannot clean up dnf_pkg '$target' — no SSH key for host '$host'"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if cleanup_dnf_pkg "$ssh_key" "$target"; then
|
||||||
|
CLEANED=$((CLEANED + 1))
|
||||||
|
else
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
static_bin)
|
||||||
|
if [[ -z "$ssh_key" ]]; then
|
||||||
|
log_warn "Cannot clean up static_bin '$target' — no SSH key for host '$host'"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if cleanup_static_bin "$ssh_key" "$target"; then
|
||||||
|
CLEANED=$((CLEANED + 1))
|
||||||
|
else
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
docker_group)
|
||||||
|
if [[ -z "$ssh_key" ]]; then
|
||||||
|
log_warn "Cannot clean up docker_group '$target' — no SSH key for host '$host'"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if cleanup_docker_group "$ssh_key" "$target"; then
|
||||||
|
CLEANED=$((CLEANED + 1))
|
||||||
|
else
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
systemd_svc)
|
||||||
|
if [[ -z "$ssh_key" ]]; then
|
||||||
|
log_warn "Cannot clean up systemd_svc '$target' — no SSH key for host '$host'"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if cleanup_systemd_svc "$ssh_key" "$target"; then
|
||||||
|
CLEANED=$((CLEANED + 1))
|
||||||
|
else
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
xcode_cli)
|
||||||
|
if cleanup_xcode_cli; then
|
||||||
|
CLEANED=$((CLEANED + 1))
|
||||||
|
else
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_warn "Unknown action type '${action_type}' for target '${target}' — skipping"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Clear the manifest after successful cleanup (unless dry run)
|
||||||
|
if [[ "$DRY_RUN" == "false" ]]; then
|
||||||
|
manifest_clear "$host"
|
||||||
|
log_success "Manifest cleared for ${host}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
printf '\n'
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
log_info "[dry-run] Would process ${TOTAL_ACTIONS} actions across ${#HOSTS[@]} host(s)"
|
||||||
|
else
|
||||||
|
log_info "Cleanup summary: ${CLEANED} cleaned, ${FAILED} failed (out of ${TOTAL_ACTIONS} actions)"
|
||||||
|
if [[ $FAILED -gt 0 ]]; then
|
||||||
|
log_warn "Some cleanup actions failed — check logs above"
|
||||||
|
else
|
||||||
|
log_success "All cleanup actions completed successfully"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -22,6 +22,11 @@ log_info "Verifying Fedora OS..."
|
|||||||
require_remote_os "FEDORA" "Linux" "Fedora target must be a Linux machine — check FEDORA_IP in .env"
|
require_remote_os "FEDORA" "Linux" "Fedora target must be a Linux machine — check FEDORA_IP in .env"
|
||||||
require_remote_pkg_manager "FEDORA" "dnf" "Fedora target must have dnf (RPM-based distro) — this script won't work on Debian/Ubuntu"
|
require_remote_pkg_manager "FEDORA" "dnf" "Fedora target must have dnf (RPM-based distro) — this script won't work on Debian/Ubuntu"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Manifest — track what we install for rollback/cleanup
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
manifest_init "fedora"
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# SSH connectivity
|
# SSH connectivity
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -40,12 +45,19 @@ if ssh_exec FEDORA "docker --version" &>/dev/null; then
|
|||||||
else
|
else
|
||||||
log_info "Installing Docker CE on Fedora..."
|
log_info "Installing Docker CE on Fedora..."
|
||||||
ssh_exec FEDORA "sudo dnf -y install dnf-plugins-core"
|
ssh_exec FEDORA "sudo dnf -y install dnf-plugins-core"
|
||||||
|
manifest_record "fedora" "dnf_pkg" "dnf-plugins-core"
|
||||||
ssh_exec FEDORA "sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
|
ssh_exec FEDORA "sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||||
ssh_exec FEDORA "sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin"
|
ssh_exec FEDORA "sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin"
|
||||||
|
manifest_record "fedora" "dnf_pkg" "docker-ce"
|
||||||
|
manifest_record "fedora" "dnf_pkg" "docker-ce-cli"
|
||||||
|
manifest_record "fedora" "dnf_pkg" "containerd.io"
|
||||||
|
manifest_record "fedora" "dnf_pkg" "docker-compose-plugin"
|
||||||
ssh_exec FEDORA "sudo systemctl enable --now docker"
|
ssh_exec FEDORA "sudo systemctl enable --now docker"
|
||||||
|
manifest_record "fedora" "systemd_svc" "docker"
|
||||||
|
|
||||||
# Add user to docker group
|
# Add user to docker group
|
||||||
ssh_exec FEDORA "sudo usermod -aG docker $FEDORA_SSH_USER"
|
ssh_exec FEDORA "sudo usermod -aG docker $FEDORA_SSH_USER"
|
||||||
|
manifest_record "fedora" "docker_group" "$FEDORA_SSH_USER"
|
||||||
log_warn "User $FEDORA_SSH_USER added to docker group. You may need to re-login for this to take effect."
|
log_warn "User $FEDORA_SSH_USER added to docker group. You may need to re-login for this to take effect."
|
||||||
|
|
||||||
if ssh_exec FEDORA "docker --version" &>/dev/null; then
|
if ssh_exec FEDORA "docker --version" &>/dev/null; then
|
||||||
@@ -74,6 +86,7 @@ if ssh_exec FEDORA "jq --version" &>/dev/null; then
|
|||||||
else
|
else
|
||||||
log_info "Installing jq on Fedora..."
|
log_info "Installing jq on Fedora..."
|
||||||
ssh_exec FEDORA "sudo dnf -y install jq"
|
ssh_exec FEDORA "sudo dnf -y install jq"
|
||||||
|
manifest_record "fedora" "dnf_pkg" "jq"
|
||||||
if ssh_exec FEDORA "jq --version" &>/dev/null; then
|
if ssh_exec FEDORA "jq --version" &>/dev/null; then
|
||||||
log_success "jq installed on Fedora"
|
log_success "jq installed on Fedora"
|
||||||
else
|
else
|
||||||
@@ -82,6 +95,14 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Minimum version checks for remote tools
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
log_info "Checking minimum versions on Fedora..."
|
||||||
|
check_remote_min_version "FEDORA" "docker" "docker --version" "20.0"
|
||||||
|
check_remote_min_version "FEDORA" "docker-compose" "docker compose version" "2.0"
|
||||||
|
check_remote_min_version "FEDORA" "jq" "jq --version" "1.6"
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Verify Docker works without sudo
|
# Verify Docker works without sudo
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ log_info "=== MacBook Setup ==="
|
|||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
require_local_os "Darwin" "macbook.sh must run on macOS — detected a non-macOS system"
|
require_local_os "Darwin" "macbook.sh must run on macOS — detected a non-macOS system"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Manifest — track what we install for rollback/cleanup
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
manifest_init "macbook"
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Homebrew
|
# Homebrew
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -37,10 +42,21 @@ for pkg in "${BREW_PACKAGES[@]}"; do
|
|||||||
else
|
else
|
||||||
log_info "Installing $pkg..."
|
log_info "Installing $pkg..."
|
||||||
brew install "$pkg"
|
brew install "$pkg"
|
||||||
|
manifest_record "macbook" "brew_pkg" "$pkg"
|
||||||
log_success "$pkg installed"
|
log_success "$pkg installed"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Minimum version checks for local tools
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
log_info "Checking minimum versions..."
|
||||||
|
check_min_version "jq" "jq --version" "1.6"
|
||||||
|
check_min_version "curl" "curl --version" "7.70"
|
||||||
|
check_min_version "git" "git --version" "2.30"
|
||||||
|
check_min_version "shellcheck" "shellcheck --version" "0.8"
|
||||||
|
check_min_version "gh" "gh --version" "2.0"
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Verify built-in tools
|
# Verify built-in tools
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -70,6 +86,7 @@ if xcode-select -p &>/dev/null; then
|
|||||||
else
|
else
|
||||||
log_info "Installing Xcode Command Line Tools..."
|
log_info "Installing Xcode Command Line Tools..."
|
||||||
xcode-select --install
|
xcode-select --install
|
||||||
|
manifest_record "macbook" "xcode_cli" "xcode-select"
|
||||||
log_warn "Xcode CLI Tools installation started. Wait for it to finish, then re-run this script."
|
log_warn "Xcode CLI Tools installation started. Wait for it to finish, then re-run this script."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ log_info "=== Unraid Setup ==="
|
|||||||
log_info "Verifying Unraid OS..."
|
log_info "Verifying Unraid OS..."
|
||||||
require_remote_os "UNRAID" "Linux" "Unraid must be a Linux machine — check UNRAID_IP in .env"
|
require_remote_os "UNRAID" "Linux" "Unraid must be a Linux machine — check UNRAID_IP in .env"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Manifest — track what we install for rollback/cleanup
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
manifest_init "unraid"
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# SSH connectivity
|
# SSH connectivity
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -52,6 +57,7 @@ else
|
|||||||
log_info "Installing docker-compose standalone binary on Unraid..."
|
log_info "Installing docker-compose standalone binary on Unraid..."
|
||||||
COMPOSE_VERSION="v2.29.1"
|
COMPOSE_VERSION="v2.29.1"
|
||||||
ssh_exec UNRAID "curl -SL https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose"
|
ssh_exec UNRAID "curl -SL https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose"
|
||||||
|
manifest_record "unraid" "static_bin" "/usr/local/bin/docker-compose"
|
||||||
if ssh_exec UNRAID "docker-compose --version" &>/dev/null; then
|
if ssh_exec UNRAID "docker-compose --version" &>/dev/null; then
|
||||||
log_success "docker-compose installed on Unraid"
|
log_success "docker-compose installed on Unraid"
|
||||||
else
|
else
|
||||||
@@ -69,6 +75,7 @@ else
|
|||||||
log_info "Installing jq static binary on Unraid..."
|
log_info "Installing jq static binary on Unraid..."
|
||||||
JQ_VERSION="1.7.1"
|
JQ_VERSION="1.7.1"
|
||||||
ssh_exec UNRAID "curl -SL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64 -o /usr/local/bin/jq && chmod +x /usr/local/bin/jq"
|
ssh_exec UNRAID "curl -SL https://github.com/jqlang/jq/releases/download/jq-${JQ_VERSION}/jq-linux-amd64 -o /usr/local/bin/jq && chmod +x /usr/local/bin/jq"
|
||||||
|
manifest_record "unraid" "static_bin" "/usr/local/bin/jq"
|
||||||
if ssh_exec UNRAID "jq --version" &>/dev/null; then
|
if ssh_exec UNRAID "jq --version" &>/dev/null; then
|
||||||
log_success "jq installed on Unraid"
|
log_success "jq installed on Unraid"
|
||||||
else
|
else
|
||||||
@@ -77,6 +84,14 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Minimum version checks for remote tools
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
log_info "Checking minimum versions on Unraid..."
|
||||||
|
check_remote_min_version "UNRAID" "docker" "docker --version" "20.0"
|
||||||
|
check_remote_min_version "UNRAID" "docker-compose" "docker compose version 2>/dev/null || docker-compose --version" "2.0"
|
||||||
|
check_remote_min_version "UNRAID" "jq" "jq --version" "1.6"
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
# Data path
|
# Data path
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ source "${SCRIPT_DIR}/lib/common.sh"
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
THROUGH=1
|
THROUGH=1
|
||||||
AUTO_YES=false
|
AUTO_YES=false
|
||||||
|
RUN_CLEANUP=false
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
@@ -29,6 +30,7 @@ for arg in "$@"; do
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
--cleanup) RUN_CLEANUP=true ;;
|
||||||
--yes|-y) AUTO_YES=true ;;
|
--yes|-y) AUTO_YES=true ;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -36,12 +38,14 @@ Usage: $(basename "$0") [options]
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--through=N Only tear down phases N through 9 (default: 1 = everything)
|
--through=N Only tear down phases N through 9 (default: 1 = everything)
|
||||||
|
--cleanup Also run setup/cleanup.sh to uninstall setup prerequisites
|
||||||
--yes, -y Skip all confirmation prompts
|
--yes, -y Skip all confirmation prompts
|
||||||
--help Show this help
|
--help Show this help
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
$(basename "$0") Tear down everything
|
$(basename "$0") Tear down everything
|
||||||
$(basename "$0") --through=5 Tear down phases 5-9, leave 1-4
|
$(basename "$0") --through=5 Tear down phases 5-9, leave 1-4
|
||||||
|
$(basename "$0") --cleanup Full teardown + uninstall prerequisites
|
||||||
$(basename "$0") --yes Non-interactive teardown
|
$(basename "$0") --yes Non-interactive teardown
|
||||||
EOF
|
EOF
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
@@ -121,6 +125,24 @@ for entry in "${TEARDOWNS[@]}"; do
|
|||||||
printf '\n'
|
printf '\n'
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Optional: Run setup/cleanup.sh to uninstall prerequisites
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [[ "$RUN_CLEANUP" == "true" ]]; then
|
||||||
|
log_info ">>> Running setup cleanup (uninstalling prerequisites)..."
|
||||||
|
cleanup_args=()
|
||||||
|
if [[ "$AUTO_YES" == "true" ]]; then
|
||||||
|
cleanup_args+=(--yes)
|
||||||
|
fi
|
||||||
|
if "${SCRIPT_DIR}/setup/cleanup.sh" "${cleanup_args[@]}"; then
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
log_warn "Setup cleanup had issues (continuing)"
|
||||||
|
FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Summary
|
# Summary
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user