Files
hops/uninstall
T
Stephen Klein a7c38cd58d Fix critical and high bugs B1-B6
- B1: Replace recursive get_timezone_mount/get_gpu_devices with literal YAML strings
- B3: Expand /home/*/hops glob via compgen -G instead of storing as array literal;
  fix eval echo ~$SUDO_USER -> getent passwd in uninstall
- B4: Correct services source path in setup_firewall (hops_service_definitions.sh -> services)
- B5: Replace all ((x++)) with x=$((x + 1)) to avoid set -e abort on zero pre-increment
- B6: Add Linux-only guard at top of hops entry point

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 21:54:46 -04:00

560 lines
22 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
uninstall_hops() {
# Clear terminal at startup
clear
# Exit on any error (but allow some failures during cleanup)
set +e
# Script version for consistency
local SCRIPT_VERSION="1.0.0"
# --------------------------------------------
# LOGGING SETUP
# --------------------------------------------
local LOG_DIR="/var/log/hops"
local LOG_FILE="$LOG_DIR/hops-uninstall-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
log() {
echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE"
}
error_exit() {
log "❌ ERROR: $1"
log "❌ Uninstallation failed. Check logs at: $LOG_FILE"
exit 1
}
warning() {
log "⚠️ WARNING: $1"
}
# --------------------------------------------
# HEADER
# --------------------------------------------
cat << "EOF"
_ _ ____ ____ ____
| | | || _ \| _ \/ ___|
| |__| || |_) | |_) \___ \
| __ || __/| __/ ___) |
|_| |_||_| |_| |____/
EOF
echo -e "🗑️ Homelab Orchestration Provisioning Script - UNINSTALLER v${SCRIPT_VERSION}\n"
log "🗑️ Starting HOPS Uninstallation v${SCRIPT_VERSION}"
# --------------------------------------------
# ROOT CHECK
# --------------------------------------------
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root or with sudo."
fi
# --------------------------------------------
# CONFIRMATION PROMPT
# --------------------------------------------
show_uninstall_warning() {
echo -e "⚠️ WARNING: This will completely remove your HOPS installation!"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "This uninstaller will:"
echo -e " • Stop and remove all Docker containers"
echo -e " • Remove Docker images (optional)"
echo -e " • Remove Docker Compose configuration"
echo -e " • Clean up application data (optional)"
echo -e " • Remove firewall rules"
echo -e " • Uninstall Docker (optional)"
echo -e ""
echo -e "⚠️ YOUR MEDIA FILES WILL NOT BE DELETED"
echo -e "⚠️ APPLICATION DATA REMOVAL IS OPTIONAL"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}
get_uninstall_options() {
echo -e "\n🔧 Uninstall Options:"
# Container and compose removal (always done)
REMOVE_CONTAINERS=true
REMOVE_COMPOSE=true
# Optional removals
echo -e "\n❓ Remove Docker images? (saves disk space but requires re-download) [y/N]: "
read -r remove_images
REMOVE_IMAGES=false
[[ "$remove_images" =~ ^[Yy]$ ]] && REMOVE_IMAGES=true
echo -e "\n❓ Remove application data? (⚠️ DELETES ALL CONFIGURATIONS!) [y/N]: "
read -r remove_appdata
REMOVE_APPDATA=false
[[ "$remove_appdata" =~ ^[Yy]$ ]] && REMOVE_APPDATA=true
echo -e "\n❓ Uninstall Docker completely? [y/N]: "
read -r remove_docker
REMOVE_DOCKER=false
[[ "$remove_docker" =~ ^[Yy]$ ]] && REMOVE_DOCKER=true
echo -e "\n❓ Remove firewall rules? [Y/n]: "
read -r remove_firewall
REMOVE_FIREWALL=true
[[ "$remove_firewall" =~ ^[Nn]$ ]] && REMOVE_FIREWALL=false
# Final confirmation
echo -e "\n⚠️ FINAL CONFIRMATION"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━"
echo -e "Actions to perform:"
echo -e " • Remove containers: ✅"
echo -e " • Remove compose files: ✅"
[[ $REMOVE_IMAGES == true ]] && echo -e " • Remove Docker images: ✅" || echo -e " • Remove Docker images: ❌"
[[ $REMOVE_APPDATA == true ]] && echo -e " • Remove app data: ✅" || echo -e " • Remove app data: ❌"
[[ $REMOVE_DOCKER == true ]] && echo -e " • Uninstall Docker: ✅" || echo -e " • Uninstall Docker: ❌"
[[ $REMOVE_FIREWALL == true ]] && echo -e " • Remove firewall rules: ✅" || echo -e " • Remove firewall rules: ❌"
echo -e "\n❓ Proceed with uninstallation? [y/N]: "
read -r final_confirm
if [[ ! "$final_confirm" =~ ^[Yy]$ ]]; then
log "🚫 Uninstallation cancelled by user"
exit 0
fi
}
# --------------------------------------------
# HOMELAB DIRECTORY DETECTION
# --------------------------------------------
find_homelab_directory() {
local POSSIBLE_DIRS=(
"$HOME/hops"
"/opt/hops"
"/srv/hops"
)
while IFS= read -r d; do POSSIBLE_DIRS+=("$d"); done < <(compgen -G "/home/*/hops" 2>/dev/null)
if [[ -n "$SUDO_USER" ]]; then
local user_home
user_home=$(getent passwd "$SUDO_USER" | cut -d: -f6)
POSSIBLE_DIRS=("$user_home/hops" "${POSSIBLE_DIRS[@]}")
fi
HOMELAB_DIR=""
for dir in "${POSSIBLE_DIRS[@]}"; do
if [[ -f "$dir/docker-compose.yml" ]]; then
HOMELAB_DIR="$dir"
log "✅ Found homelab directory: $HOMELAB_DIR"
break
fi
done
if [[ -z "$HOMELAB_DIR" ]]; then
echo -e "\n📂 Could not auto-detect homelab directory."
echo -e "Please enter the path to your homelab directory (contains docker-compose.yml):"
read -r user_dir
if [[ -f "$user_dir/docker-compose.yml" ]]; then
HOMELAB_DIR="$user_dir"
log "✅ Using homelab directory: $HOMELAB_DIR"
else
warning "No docker-compose.yml found in $user_dir"
log "📝 Will proceed with container cleanup by name instead"
fi
fi
# Set APPDATA_DIR from env file if available
if [[ -f "$HOMELAB_DIR/.env" ]]; then
APPDATA_DIR=$(grep "^CONFIG_ROOT=" "$HOMELAB_DIR/.env" | cut -d= -f2)
log "📁 Found appdata directory: $APPDATA_DIR"
fi
}
# --------------------------------------------
# SERVICE DETECTION
# --------------------------------------------
detect_running_services() {
log "🔍 Detecting running HOPS services..."
# Known HOPS service names
local KNOWN_SERVICES=(
"sonarr" "radarr" "lidarr" "readarr" "bazarr" "prowlarr" "tdarr"
"nzbget" "sabnzbd" "transmission" "qbittorrent"
"plex" "jellyfin" "emby" "jellystat" "jellystat-db"
"overseerr" "jellyseerr" "ombi"
"traefik" "nginx-proxy-manager" "authelia"
"portainer" "watchtower" "uptime-kuma"
"postgres" "redis"
)
DETECTED_SERVICES=()
for service in "${KNOWN_SERVICES[@]}"; do
if docker ps -a --format "{{.Names}}" | grep -q "^${service}$"; then
DETECTED_SERVICES+=("$service")
fi
done
if [[ ${#DETECTED_SERVICES[@]} -gt 0 ]]; then
log "✅ Detected services: ${DETECTED_SERVICES[*]}"
else
log "⚠️ No HOPS services detected"
fi
}
# --------------------------------------------
# CONTAINER CLEANUP
# --------------------------------------------
stop_and_remove_containers() {
log "🛑 Stopping and removing containers..."
if [[ -n "$HOMELAB_DIR" && -f "$HOMELAB_DIR/docker-compose.yml" ]]; then
cd "$HOMELAB_DIR"
# Stop services gracefully
log "🔄 Stopping services with docker compose..."
if docker compose ps -q | grep -q .; then
if ! docker compose down --timeout 30 2>&1 | tee -a "$LOG_FILE"; then
warning "Docker compose down failed, attempting force removal"
docker compose down --timeout 10 --remove-orphans --volumes 2>&1 | tee -a "$LOG_FILE" || true
fi
else
log "️ No running compose services found"
fi
fi
# Fallback: Remove containers by name
if [[ ${#DETECTED_SERVICES[@]} -gt 0 ]]; then
log "🧹 Cleaning up remaining containers..."
for service in "${DETECTED_SERVICES[@]}"; do
if docker ps -a --format "{{.Names}}" | grep -q "^${service}$"; then
log "🗑️ Removing container: $service"
docker stop "$service" 2>/dev/null || true
docker rm -f "$service" 2>/dev/null || true
fi
done
fi
log "✅ Container cleanup complete"
}
# --------------------------------------------
# IMAGE CLEANUP
# --------------------------------------------
remove_docker_images() {
if [[ $REMOVE_IMAGES == true ]]; then
log "🗑️ Removing Docker images..."
# Common HOPS images
local HOPS_IMAGES=(
"lscr.io/linuxserver/sonarr"
"lscr.io/linuxserver/radarr"
"lscr.io/linuxserver/lidarr"
"lscr.io/linuxserver/readarr"
"lscr.io/linuxserver/bazarr"
"lscr.io/linuxserver/prowlarr"
"ghcr.io/haveagitgat/tdarr"
"lscr.io/linuxserver/nzbget"
"lscr.io/linuxserver/sabnzbd"
"lscr.io/linuxserver/transmission"
"lscr.io/linuxserver/qbittorrent"
"plexinc/pms-docker"
"jellyfin/jellyfin"
"emby/embyserver"
"sctx/overseerr"
"fallenbagel/jellyseerr"
"lscr.io/linuxserver/ombi"
"traefik"
"jc21/nginx-proxy-manager"
"authelia/authelia"
"portainer/portainer-ce"
"containrrr/watchtower"
"louislam/uptime-kuma"
"postgres"
"redis"
"cyfershepard/jellystat"
)
for image in "${HOPS_IMAGES[@]}"; do
if docker images --format "{{.Repository}}" | grep -q "^${image}$"; then
log "🗑️ Removing image: $image"
docker rmi -f "$image" 2>/dev/null || true
fi
done
# Clean up dangling images
log "🧹 Cleaning up dangling images..."
docker image prune -f 2>/dev/null || true
log "✅ Image cleanup complete"
else
log "⏭️ Skipping image removal"
fi
}
# --------------------------------------------
# NETWORK CLEANUP
# --------------------------------------------
cleanup_networks() {
log "🌐 Cleaning up Docker networks..."
local HOPS_NETWORKS=("homelab" "traefik" "database")
for network in "${HOPS_NETWORKS[@]}"; do
if docker network ls --format "{{.Name}}" | grep -q "^${network}$"; then
log "🗑️ Removing network: $network"
docker network rm "$network" 2>/dev/null || warning "Could not remove network: $network"
fi
done
log "✅ Network cleanup complete"
}
# --------------------------------------------
# VOLUME CLEANUP
# --------------------------------------------
cleanup_volumes() {
log "💾 Cleaning up Docker volumes..."
local HOPS_VOLUMES=("postgres_data" "redis_data")
for volume in "${HOPS_VOLUMES[@]}"; do
if docker volume ls --format "{{.Name}}" | grep -q "^${volume}$"; then
log "🗑️ Removing volume: $volume"
docker volume rm "$volume" 2>/dev/null || warning "Could not remove volume: $volume"
fi
done
# Clean up orphaned volumes
log "🧹 Cleaning up orphaned volumes..."
docker volume prune -f 2>/dev/null || true
log "✅ Volume cleanup complete"
}
# --------------------------------------------
# FILE CLEANUP
# --------------------------------------------
cleanup_compose_files() {
if [[ $REMOVE_COMPOSE == true && -n "$HOMELAB_DIR" ]]; then
log "📝 Removing Docker Compose files..."
cd "$HOMELAB_DIR"
# Backup before removal
if [[ -f docker-compose.yml ]]; then
local BACKUP_FILE="docker-compose.yml.removed.$(date +%Y%m%d%H%M%S)"
log "📦 Backing up compose file to: $BACKUP_FILE"
cp docker-compose.yml "$BACKUP_FILE" 2>/dev/null || warning "Could not backup compose file"
rm -f docker-compose.yml
fi
# Remove other compose-related files
rm -f docker-compose.override.yml .env 2>/dev/null || true
# Remove empty homelab directory if it's empty
cd ..
if [[ -d "$HOMELAB_DIR" ]]; then
rmdir "$HOMELAB_DIR" 2>/dev/null && log "📁 Removed empty homelab directory" || log "📁 Homelab directory not empty, keeping it"
fi
log "✅ Compose file cleanup complete"
else
log "⏭️ Skipping compose file removal"
fi
}
cleanup_appdata() {
if [[ $REMOVE_APPDATA == true && -n "$APPDATA_DIR" ]]; then
log "🗂️ Removing application data..."
echo -e "\n⚠️ FINAL WARNING: This will delete ALL application configurations!"
echo -e "Application data directory: $APPDATA_DIR"
echo -e "❓ Are you absolutely sure? Type 'DELETE' to confirm: "
read -r delete_confirm
if [[ "$delete_confirm" == "DELETE" ]]; then
# Create a backup first
local BACKUP_DIR="/tmp/hops-appdata-backup-$(date +%Y%m%d%H%M%S)"
log "📦 Creating backup at: $BACKUP_DIR"
if cp -r "$APPDATA_DIR" "$BACKUP_DIR" 2>/dev/null; then
log "✅ Backup created successfully"
# Remove the original
if rm -rf "$APPDATA_DIR" 2>/dev/null; then
log "✅ Application data removed"
log "📦 Backup available at: $BACKUP_DIR"
else
warning "Failed to remove application data directory"
fi
else
warning "Could not create backup, skipping appdata removal"
fi
else
log "🚫 Application data removal cancelled"
fi
else
log "⏭️ Skipping application data removal"
fi
}
# --------------------------------------------
# FIREWALL CLEANUP
# --------------------------------------------
cleanup_firewall() {
if [[ $REMOVE_FIREWALL == true ]]; then
log "🔥 Removing firewall rules..."
if command -v ufw &>/dev/null; then
# Remove HOPS-specific rules by searching for comments
local rules_to_remove=()
# Get numbered rules that contain HOPS service names
mapfile -t rules_to_remove < <(ufw status numbered | grep -E "(sonarr|radarr|lidarr|readarr|bazarr|prowlarr|jellyfin|plex|portainer|traefik|npm)" | awk '{print $1}' | tr -d '[]')
# Remove rules in reverse order to maintain numbering
if [[ ${#rules_to_remove[@]} -gt 0 ]]; then
for ((i=${#rules_to_remove[@]}-1; i>=0; i--)); do
local rule_num="${rules_to_remove[i]}"
log "🗑️ Removing firewall rule #$rule_num"
echo "y" | ufw delete "$rule_num" 2>/dev/null || true
done
fi
log "✅ Firewall cleanup complete"
else
log "️ UFW not installed, skipping firewall cleanup"
fi
else
log "⏭️ Skipping firewall cleanup"
fi
}
# --------------------------------------------
# DOCKER UNINSTALLATION
# --------------------------------------------
uninstall_docker() {
if [[ $REMOVE_DOCKER == true ]]; then
log "🐳 Uninstalling Docker..."
# Stop Docker service
systemctl stop docker 2>/dev/null || true
systemctl disable docker 2>/dev/null || true
# Remove Docker packages
local DOCKER_PACKAGES=(
"docker-ce" "docker-ce-cli" "containerd.io" "docker-buildx-plugin"
"docker-compose-plugin" "docker.io" "docker-doc" "docker-compose"
"podman-docker" "containerd" "runc"
)
for package in "${DOCKER_PACKAGES[@]}"; do
if dpkg -l | grep -q "^ii.*$package"; then
log "🗑️ Removing package: $package"
apt-get remove -y "$package" 2>/dev/null || true
fi
done
# Clean up Docker directories
log "🗑️ Removing Docker directories..."
rm -rf /var/lib/docker /etc/docker /var/run/docker.sock 2>/dev/null || true
rm -rf ~/.docker 2>/dev/null || true
# Remove from user directories too
if [[ -n "$SUDO_USER" ]]; then
local user_home=$(eval echo "~$SUDO_USER")
rm -rf "$user_home/.docker" 2>/dev/null || true
fi
# Remove Docker group
if getent group docker &>/dev/null; then
groupdel docker 2>/dev/null || warning "Could not remove docker group"
fi
# Clean up package cache
apt-get autoremove -y 2>/dev/null || true
apt-get autoclean 2>/dev/null || true
log "✅ Docker uninstallation complete"
else
log "⏭️ Skipping Docker uninstallation"
fi
}
# --------------------------------------------
# CLEANUP LOG FILES
# --------------------------------------------
cleanup_logs() {
log "📋 Cleaning up HOPS log files..."
# Keep current log file, remove others
find "$LOG_DIR" -name "homelab-*.log" -not -name "$(basename "$LOG_FILE")" -delete 2>/dev/null || true
# Remove log directory if empty (except current log)
local remaining_logs=$(find "$LOG_DIR" -name "*.log" | wc -l)
if [[ $remaining_logs -le 1 ]]; then
log "📁 Log directory will be cleaned after this session"
fi
log "✅ Log cleanup complete"
}
# --------------------------------------------
# MAIN UNINSTALLATION FLOW
# --------------------------------------------
show_uninstall_warning
get_uninstall_options
log "🚀 Starting HOPS uninstallation process..."
find_homelab_directory
detect_running_services
# Core cleanup steps
stop_and_remove_containers
remove_docker_images
cleanup_networks
cleanup_volumes
cleanup_compose_files
cleanup_appdata
cleanup_firewall
uninstall_docker
cleanup_logs
# --------------------------------------------
# FINAL SUMMARY
# --------------------------------------------
echo -e "\n✅ HOPS Uninstallation Complete!"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "📋 Uninstallation Summary:"
echo -e "\n🗑️ Removed Components:"
echo " • Docker containers: ✅"
echo " • Docker Compose files: ✅"
[[ $REMOVE_IMAGES == true ]] && echo " • Docker images: ✅" || echo " • Docker images: ❌ (kept)"
[[ $REMOVE_APPDATA == true ]] && echo " • Application data: ✅" || echo " • Application data: ❌ (kept)"
[[ $REMOVE_DOCKER == true ]] && echo " • Docker installation: ✅" || echo " • Docker installation: ❌ (kept)"
[[ $REMOVE_FIREWALL == true ]] && echo " • Firewall rules: ✅" || echo " • Firewall rules: ❌ (kept)"
echo -e "\n📂 Preserved:"
echo " • Media files: ✅ (never touched)"
[[ $REMOVE_APPDATA != true ]] && echo " • Application configurations: ✅"
if [[ $REMOVE_APPDATA == true ]]; then
echo -e "\n📦 Backup Location:"
echo " • Application data backup: /tmp/hops-appdata-backup-*"
echo " • Consider moving this backup to a permanent location"
fi
echo -e "\n📋 Complete log: $LOG_FILE"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "✅ HOPS uninstallation completed successfully!"
if [[ -n "$SUDO_USER" ]]; then
echo -e "\n💡 Note: You may want to restart your session to ensure all group changes take effect."
fi
if [[ $REMOVE_DOCKER == true ]]; then
echo -e "\n🔄 Recommendation: Reboot your system to complete Docker removal."
fi
return 0
}