#!/usr/bin/env bash set -euo pipefail # ============================================================================= # setup/cleanup.sh — Reverse everything installed by setup scripts # Reads .manifests/.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 </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