347 lines
11 KiB
Bash
Executable File
347 lines
11 KiB
Bash
Executable File
#!/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)"
|
|
# shellcheck source=../lib/common.sh
|
|
# shellcheck disable=SC1091
|
|
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)"
|
|
}
|
|
|
|
cleanup_ssh_key() {
|
|
local host_key="$1" path="$2"
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[dry-run] Would remove SSH key pair on ${host_key}: ${path}"
|
|
return 0
|
|
fi
|
|
log_info "Removing SSH key pair on ${host_key}: ${path}"
|
|
# No single quotes around path — tilde must expand on the remote shell
|
|
ssh_exec "$host_key" "rm -f ${path} ${path}.pub" 2>/dev/null || log_warn "Failed to remove SSH key on ${host_key}"
|
|
}
|
|
|
|
cleanup_authorized_key() {
|
|
local host_key="$1" marker="$2"
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[dry-run] Would remove authorized_key entry '${marker}' on ${host_key}"
|
|
return 0
|
|
fi
|
|
log_info "Removing authorized_key entry '${marker}' on ${host_key}"
|
|
ssh_exec "$host_key" "sed -i '/# ${marker}/d' ~/.ssh/authorized_keys" 2>/dev/null || log_warn "Failed to remove authorized_key '${marker}' on ${host_key}"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|
|
;;
|
|
ssh_key)
|
|
if [[ -z "$ssh_key" ]]; then
|
|
log_warn "Cannot clean up ssh_key '$target' — no SSH key for host '$host'"
|
|
FAILED=$((FAILED + 1))
|
|
continue
|
|
fi
|
|
if cleanup_ssh_key "$ssh_key" "$target"; then
|
|
CLEANED=$((CLEANED + 1))
|
|
else
|
|
FAILED=$((FAILED + 1))
|
|
fi
|
|
;;
|
|
authorized_key)
|
|
if [[ -z "$ssh_key" ]]; then
|
|
log_warn "Cannot clean up authorized_key '$target' — no SSH key for host '$host'"
|
|
FAILED=$((FAILED + 1))
|
|
continue
|
|
fi
|
|
if cleanup_authorized_key "$ssh_key" "$target"; 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
|