Initial release of HOPS v3.1.0

- Complete homelab orchestration and provisioning system
- Support for 20+ popular homelab services
- Interactive installation with dependency resolution
- Security hardening and firewall configuration
- Service health monitoring and management interface
- Comprehensive error handling with rollback capabilities
- Complete uninstaller with data preservation options

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Stephen Klein
2025-07-12 06:42:48 -04:00
commit 772fb7da52
10 changed files with 7257 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 HOPS Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+339
View File
@@ -0,0 +1,339 @@
# HOPS - Homelab Orchestration Provisioning Script
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Version](https://img.shields.io/badge/Version-3.1.0-blue.svg)]()
[![Platform](https://img.shields.io/badge/Platform-Ubuntu%2FDebian%2FMint-orange.svg)]()
**HOPS** is a comprehensive, automated deployment solution for popular homelab applications. It simplifies the process of setting up and managing Docker-based services including media servers, download clients, monitoring tools, and more.
## 🎯 What is HOPS?
HOPS (Homelab Orchestration Provisioning Script) automates the deployment of a complete homelab infrastructure using Docker Compose. It provides an intuitive menu-driven interface for selecting, configuring, and managing services with enterprise-grade features like:
- **Automated dependency resolution**
- **Security hardening and firewall configuration**
- **Service health monitoring**
- **Rollback capabilities on failure**
- **Comprehensive logging**
- **User-friendly management interface**
## ✨ Key Features
### 🚀 **Easy Installation**
- One-command installation process
- Automatic Docker installation and configuration
- Interactive service selection
- Intelligent dependency resolution
### 🔒 **Security First**
- Automatic firewall configuration
- Secure password generation
- File permission hardening
- Network isolation
### 📊 **Management & Monitoring**
- Real-time service status monitoring
- Centralized log viewing
- Easy service management (start/stop/restart)
- Health checks and service verification
### 🔄 **Reliability**
- Error handling with automatic rollback
- Service dependency management
- Port conflict detection
- System requirements validation
## 📱 Supported Services
### 📺 Media Management (*arr Stack)
- **Sonarr** - TV show management
- **Radarr** - Movie management
- **Lidarr** - Music management
- **Readarr** - eBook/audiobook management
- **Bazarr** - Subtitle management
- **Prowlarr** - Indexer management
- **Tdarr** - Media transcoding
### ⬇️ Download Clients
- **qBittorrent** - Feature-rich BitTorrent client
- **Transmission** - Lightweight BitTorrent client
- **NZBGet** - Efficient Usenet downloader
- **SABnzbd** - Popular Usenet client
### 🎞️ Media Servers
- **Jellyfin** - Open-source media server
- **Plex** - Popular media server platform
- **Emby** - Feature-rich media server
- **Jellystat** - Jellyfin statistics and monitoring
### 🎛️ Request Management
- **Overseerr** - Media request management for Plex
- **Jellyseerr** - Media request management for Jellyfin
- **Ombi** - Media request platform
### 🔒 Reverse Proxy & Security
- **Traefik** - Modern reverse proxy with automatic SSL
- **Nginx Proxy Manager** - Easy-to-use reverse proxy
- **Authelia** - Authentication and authorization server
### 📈 Monitoring & Management
- **Portainer** - Docker container management
- **Uptime Kuma** - Service monitoring
- **Watchtower** - Automatic container updates
## 🔧 System Requirements
### Minimum Requirements
- **OS**: Ubuntu 20.04+, Debian 11+, or Linux Mint 20+
- **RAM**: 2GB (4GB+ recommended)
- **Storage**: 10GB free space (more for media)
- **CPU**: 2 cores recommended
- **Network**: Internet connection required
### Prerequisites
- Root/sudo access
- x86_64 architecture
## 🚀 Quick Start
### 1. Download HOPS
```bash
git clone https://github.com/yourusername/hops.git
cd hops
chmod +x hops.sh
```
### 2. Run Installation
```bash
sudo ./hops.sh
```
### 3. Follow the Interactive Setup
- Select your desired services
- Configure directories and timezone
- Choose security options
- Wait for automated deployment
### 4. Access Your Services
The installer will provide URLs for all deployed services:
```
📱 Access your services at:
● Jellyfin http://192.168.1.100:8096
● Sonarr http://192.168.1.100:8989
● Radarr http://192.168.1.100:7878
● Portainer http://192.168.1.100:9000
```
## 📁 Default Directory Structure
```
~/homelab/ # Main homelab directory
├── docker-compose.yml # Service definitions
├── .env # Environment variables
└── logs/ # Application logs
/opt/appdata/ # Application configurations
├── jellyfin/
├── sonarr/
├── radarr/
└── ...
/mnt/media/ # Media storage
├── movies/
├── tv/
├── music/
└── downloads/
```
## 🎛️ Management Interface
HOPS includes a comprehensive management interface accessible through the main script:
```bash
sudo ./hops.sh
```
### Available Options:
1. **Install HOPS** - Deploy new services
2. **Uninstall HOPS** - Complete removal with options
3. **Manage Services** - Start/stop/restart services
4. **Service Status** - Real-time service monitoring
5. **Access Information** - Get service URLs and credentials
6. **View Logs** - Centralized log viewing
7. **Help & Documentation** - Built-in help system
## 🔧 Advanced Configuration
### Environment Variables
All configuration is stored in `~/homelab/.env`:
```bash
# Core Configuration
PUID=1000 # User ID
PGID=1000 # Group ID
TZ=America/New_York # Timezone
# Directory Configuration
DATA_ROOT=/mnt/media # Media storage
CONFIG_ROOT=/opt/appdata # App configurations
# Security
DEFAULT_ADMIN_PASSWORD=... # Generated secure password
DEFAULT_DB_PASSWORD=... # Database password
# Optional: Custom domain
DOMAIN=yourdomain.com
ACME_EMAIL=admin@yourdomain.com
```
### Service Management Commands
```bash
# Navigate to homelab directory
cd ~/homelab
# View running services
docker compose ps
# View logs
docker compose logs -f [service-name]
# Restart specific service
docker compose restart [service-name]
# Update all services
docker compose pull && docker compose up -d
# Stop all services
docker compose down
```
## 🔒 Security Features
### Automatic Security Hardening
- **Firewall Configuration**: Automatic UFW rules for service ports
- **Secure Passwords**: Cryptographically secure password generation
- **File Permissions**: Restrictive permissions on sensitive files
- **Network Isolation**: Docker network segregation
- **SSL/TLS**: Automatic certificate management with Traefik
### Post-Installation Security
1. **Change Default Passwords**: Update passwords in `.env` file
2. **Configure Reverse Proxy**: Set up Traefik or Nginx Proxy Manager
3. **Enable Authentication**: Configure Authelia for additional security
4. **Regular Updates**: Use Watchtower for automatic updates
## 🆘 Troubleshooting
### Common Issues
#### Port Conflicts
```bash
# Check for port conflicts
sudo lsof -i :PORT_NUMBER
# View HOPS service status
sudo ./hops.sh
# Select option 4: Service Status
```
#### Service Won't Start
```bash
# Check service logs
cd ~/homelab
docker compose logs [service-name]
# Restart service
docker compose restart [service-name]
```
#### Permission Issues
```bash
# Fix ownership of data directories
sudo chown -R $USER:$USER /mnt/media /opt/appdata
```
### Log Locations
- **Installation Logs**: `/var/log/hops/`
- **Service Logs**: `docker compose logs [service-name]`
- **System Logs**: `journalctl -u docker`
### Getting Help
1. Check the built-in help: `sudo ./hops.sh` → Option 7
2. Review logs in `/var/log/hops/`
3. Verify Docker status: `systemctl status docker`
4. Check service health: `docker compose ps`
## 🔄 Backup and Recovery
### Backup Important Data
```bash
# Backup configurations
sudo tar -czf hops-config-backup.tar.gz /opt/appdata
# Backup compose files
cp ~/homelab/.env ~/homelab/docker-compose.yml /backup/location/
```
### Recovery
```bash
# Restore configurations
sudo tar -xzf hops-config-backup.tar.gz -C /
# Redeploy services
cd ~/homelab
docker compose up -d
```
## 📊 Performance Tuning
### For Low-Resource Systems
- Start with fewer services initially
- Monitor resource usage with Portainer
- Consider using lightweight alternatives (Transmission vs qBittorrent)
### For High-Performance Systems
- Enable GPU transcoding in Jellyfin/Plex
- Use SSD storage for application data
- Configure multiple download clients for redundancy
## 🤝 Contributing
We welcome contributions! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
### Development Setup
```bash
git clone https://github.com/yourusername/hops.git
cd hops
# Make changes to scripts
# Test with: sudo ./hops.sh
```
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- **LinuxServer.io** for excellent Docker images
- **Docker** for containerization platform
- **The Servarr Team** for the *arr applications
- **Jellyfin Project** for the open-source media server
- All the amazing open-source projects that make HOPS possible
## 📞 Support
- **Documentation**: Check this README and built-in help
- **Issues**: Report bugs via GitHub Issues
- **Community**: Join discussions in GitHub Discussions
---
**Made with ❤️ for the homelab community**
*HOPS - Making homelab deployment simple, secure, and reliable.*
+925
View File
@@ -0,0 +1,925 @@
#!/bin/bash
install_hops() {
# Clear terminal at startup
clear
# Exit on any error
set -e
# Script version for update tracking
local SCRIPT_VERSION="3.1.0"
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --------------------------------------------
# LOGGING SETUP
# --------------------------------------------
local LOG_DIR="/var/log/hops"
local LOG_FILE="$LOG_DIR/homelab-setup-$(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 "❌ Installation failed. Check logs at: $LOG_FILE"
exit 1
}
# Enhanced error handling with rollback
DEPLOYMENT_STEPS_COMPLETED=()
track_step() {
DEPLOYMENT_STEPS_COMPLETED+=("$1")
log "✅ Step completed: $1"
}
rollback_deployment() {
log "🔄 Rolling back deployment..."
for step in "${DEPLOYMENT_STEPS_COMPLETED[@]}"; do
case "$step" in
"containers_started")
log "🛑 Stopping containers..."
docker compose down --timeout 30 2>/dev/null || true
;;
"images_pulled")
log "🗑️ Removing pulled images..."
docker compose down --rmi all 2>/dev/null || true
;;
"directories_created")
log "📁 Cleaning up directories..."
[[ -n "$APPDATA_DIR" ]] && rm -rf "$APPDATA_DIR" 2>/dev/null || true
;;
"compose_generated")
log "📝 Removing compose file..."
[[ -f "docker-compose.yml" ]] && rm -f docker-compose.yml
;;
esac
done
log "🔄 Rollback completed"
}
error_exit_with_rollback() {
log "❌ ERROR: $1"
rollback_deployment
log "❌ Installation failed and rolled back. Check logs at: $LOG_FILE"
exit 1
}
# --------------------------------------------
# HEADER
# --------------------------------------------
cat << "EOF"
_ _ ____ ____ ____
| | | || _ \| _ \/ ___|
| |__| || |_) | |_) \___ \
| __ || __/| __/ ___) |
|_| |_||_| |_| |____/
EOF
echo -e "🚀 Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}\n"
log "🚀 Starting HOPS Deployment v${SCRIPT_VERSION}"
# --------------------------------------------
# SYSTEM REQUIREMENTS CHECK
# --------------------------------------------
check_system_requirements() {
local MIN_RAM_GB=2
local MIN_DISK_GB=10
local MIN_CORES=2
log "🔍 Checking system requirements..."
# Check RAM
local RAM_GB=$(free -g | awk '/^Mem:/{print $2}')
if [[ $RAM_GB -lt $MIN_RAM_GB ]]; then
error_exit "Insufficient RAM: ${RAM_GB}GB detected, ${MIN_RAM_GB}GB required"
fi
# Check disk space
local DISK_AVAIL=$(df -BG --output=avail / | tail -n 1 | tr -d 'G')
if [[ $DISK_AVAIL -lt $MIN_DISK_GB ]]; then
error_exit "Insufficient disk space: ${DISK_AVAIL}GB available, ${MIN_DISK_GB}GB required"
fi
# Check CPU cores
local CPU_CORES=$(nproc)
if [[ $CPU_CORES -lt $MIN_CORES ]]; then
log "⚠️ Low CPU cores: ${CPU_CORES} detected, ${MIN_CORES} recommended"
fi
log "✅ System meets minimum requirements (${RAM_GB}GB RAM, ${CPU_CORES} cores, ${DISK_AVAIL}GB disk)"
}
# --------------------------------------------
# REQUIRED PACKAGES CHECK
# --------------------------------------------
check_required_packages() {
local missing_packages=()
local required_packages=("curl" "wget" "openssl" "lsof" "apache2-utils")
log "📦 Checking required packages..."
for package in "${required_packages[@]}"; do
if ! command -v "${package%%-*}" &>/dev/null; then
missing_packages+=("$package")
fi
done
if [[ ${#missing_packages[@]} -gt 0 ]]; then
log "📦 Installing missing packages: ${missing_packages[*]}"
apt-get update && apt-get install -y "${missing_packages[@]}"
fi
log "✅ All required packages are installed"
}
# --------------------------------------------
# ROOT CHECK
# --------------------------------------------
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root or with sudo."
fi
# --------------------------------------------
# OS DETECTION
# --------------------------------------------
detect_os() {
if command -v lsb_release &>/dev/null; then
OS_NAME=$(lsb_release -is)
OS_VERSION=$(lsb_release -rs)
else
OS_NAME=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
fi
OS_NAME_LOWER=$(echo "$OS_NAME" | tr '[:upper:]' '[:lower:]')
if [[ ! "$OS_NAME_LOWER" =~ ^(ubuntu|debian|linuxmint|mint)$ ]]; then
error_exit "Unsupported OS: $OS_NAME Only Debian/Ubuntu/Mint supported"
fi
log "✅ Detected OS: $OS_NAME $OS_VERSION"
}
# --------------------------------------------
# USER CONFIGURATION COLLECTION
# --------------------------------------------
collect_user_configuration() {
log "🔧 Collecting user configuration..."
# Get running user info
if [[ -n "$SUDO_USER" ]]; then
RUNNING_USER="$SUDO_USER"
PUID=$(id -u "$SUDO_USER")
PGID=$(id -g "$SUDO_USER")
else
RUNNING_USER="root"
PUID=1000
PGID=1000
log "⚠️ Running as root, defaulting to PUID=1000, PGID=1000"
fi
# Timezone configuration
echo -e "\n🌍 Timezone Configuration"
echo "Current timezone: $(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown")"
echo -e "Keep current timezone? [Y/n]: "
read -r keep_tz
if [[ "$keep_tz" =~ ^[Nn]$ ]]; then
echo -e "Enter timezone (e.g., America/New_York, Europe/London): "
read -r user_timezone
validate_timezone "$user_timezone"
TIMEZONE="$user_timezone"
else
TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "America/New_York")
fi
# Directory configuration
echo -e "\n📁 Directory Configuration"
echo -e "Media directory [/mnt/media]: "
read -r media_dir
MEDIA_DIR="${media_dir:-/mnt/media}"
echo -e "Application data directory [/opt/appdata]: "
read -r appdata_dir
APPDATA_DIR="${appdata_dir:-/opt/appdata}"
# Create directories
mkdir -p "$MEDIA_DIR"/{movies,tv,music,books,downloads}
mkdir -p "$APPDATA_DIR"
# Set ownership if not root
if [[ "$RUNNING_USER" != "root" ]]; then
chown -R "$PUID:$PGID" "$MEDIA_DIR" "$APPDATA_DIR" 2>/dev/null || true
fi
log "✅ User configuration collected"
log " User: $RUNNING_USER ($PUID:$PGID)"
log " Timezone: $TIMEZONE"
log " Media: $MEDIA_DIR"
log " AppData: $APPDATA_DIR"
}
# --------------------------------------------
# VALIDATION FUNCTIONS
# --------------------------------------------
validate_timezone() {
if ! timedatectl list-timezones | grep -qx "$1" 2>/dev/null; then
log "⚠️ Timezone '$1' invalid, defaulting to 'America/New_York'"
TIMEZONE="America/New_York"
fi
}
validate_password() {
local password="$1"
local min_length="${2:-12}"
if [[ -z "$password" ]]; then
echo -e "\n🔐 Password must meet these requirements:"
echo " • Minimum $min_length characters"
echo " • At least one uppercase letter"
echo " • At least one lowercase letter"
echo " • At least one number"
echo " • At least one special character"
return 3
fi
if [[ ${#password} -lt $min_length ]]; then
return 1
fi
if [[ ! "$password" =~ [A-Z] ]] || [[ ! "$password" =~ [a-z] ]] || \
[[ ! "$password" =~ [0-9] ]] || [[ ! "$password" =~ [^A-Za-z0-9] ]]; then
return 2
fi
return 0
}
check_port() {
local PORT=$1
local SERVICE=$2
if lsof -i :"$PORT" >/dev/null 2>&1; then
local PROCESS=$(lsof -ti :"$PORT" | head -1)
local PROCESS_NAME=$(ps -p "$PROCESS" -o comm= 2>/dev/null || echo "unknown")
log "⚠️ Port $PORT is already in use by $PROCESS_NAME. $SERVICE may fail to start."
return 1
fi
return 0
}
check_all_ports() {
local SERVICES=("$@")
local CONFLICTS=()
# Source service definitions to get port mappings
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
fi
for svc in "${SERVICES[@]}"; do
local ports=$(get_service_ports "$svc")
for port in $ports; do
if ! check_port "$port" "$svc"; then
CONFLICTS+=("Port $port ($svc)")
fi
done
done
if [[ ${#CONFLICTS[@]} -gt 0 ]]; then
log "⚠️ Found ${#CONFLICTS[@]} port conflicts:"
for conflict in "${CONFLICTS[@]}"; do
log "$conflict"
done
echo -e "\n⚠️ Port conflicts detected! Continue anyway? (y/N): "
read -r continue_choice
if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then
error_exit "Installation cancelled due to port conflicts."
fi
return 1
fi
return 0
}
# --------------------------------------------
# DOCKER COMPOSE VERSION CHECK
# --------------------------------------------
check_docker_compose_version() {
# Check for Docker Compose plugin (v2)
if docker compose version &>/dev/null; then
log "✅ Docker Compose plugin detected ($(docker compose version --short))"
return 0
fi
# Check for standalone docker-compose (v1)
if command -v docker-compose &>/dev/null; then
log "⚠️ Found legacy docker-compose (v1). Installing Docker Compose plugin..."
if ! apt-get install -y docker-compose-plugin 2>&1 | tee -a "$LOG_FILE"; then
error_exit "Failed to install Docker Compose plugin."
fi
return 0
fi
# Neither found
error_exit "No Docker Compose detected. Please install Docker first."
}
# --------------------------------------------
# IMPROVED PASSWORD GENERATION
# --------------------------------------------
generate_secure_password() {
local length="${1:-16}"
local max_attempts=5
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
# Generate password with mixed case, numbers, and symbols
local password=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-${length})
# Ensure it meets complexity requirements
if validate_password "$password" "$length"; then
echo "$password"
return 0
fi
((attempt++))
done
# Fallback: construct a guaranteed compliant password
local upper=$(tr -dc 'A-Z' < /dev/urandom | head -c2)
local lower=$(tr -dc 'a-z' < /dev/urandom | head -c4)
local digits=$(tr -dc '0-9' < /dev/urandom | head -c2)
local symbols=$(tr -dc '!@#$%^&*' < /dev/urandom | head -c2)
local remaining_length=$((length - 10))
if [[ $remaining_length -gt 0 ]]; then
local remaining=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c$remaining_length)
echo "${upper}${lower}${digits}${symbols}${remaining}" | fold -w1 | shuf | tr -d '\n'
else
echo "${upper}${lower}${digits}${symbols}"
fi
}
# --------------------------------------------
# ENVIRONMENT FILE GENERATION
# --------------------------------------------
create_env_file() {
local homelab_dir="$1"
log "📝 Creating environment file..."
cat > "$homelab_dir/.env" <<EOF
# HOPS Environment Configuration
# Generated on $(date)
# ==============================================
# CORE CONFIGURATION
# ==============================================
# User Configuration
PUID=$PUID
PGID=$PGID
TZ=$TIMEZONE
# Directory Configuration
DATA_ROOT=$MEDIA_DIR
CONFIG_ROOT=$APPDATA_DIR
HOMELAB_DIR=$homelab_dir
# Network Configuration
DOCKER_SUBNET=172.20.0.0/16
# ==============================================
# SECURITY & AUTHENTICATION
# ==============================================
# Default Passwords (CHANGE THESE IMMEDIATELY!)
DEFAULT_ADMIN_PASSWORD=$(generate_secure_password 16)
DEFAULT_DB_PASSWORD=$(generate_secure_password 20)
# Optional: Custom domain for reverse proxy
# DOMAIN=yourdomain.com
# Optional: Email for Let's Encrypt
# ACME_EMAIL=admin@yourdomain.com
# ==============================================
# SERVICE-SPECIFIC CONFIGURATION
# ==============================================
# Plex Configuration (Get token from: https://www.plex.tv/claim/)
PLEX_CLAIM_TOKEN=
# Watchtower Email Notifications (Optional)
WATCHTOWER_EMAIL_FROM=
WATCHTOWER_EMAIL_TO=
WATCHTOWER_EMAIL_SERVER=
WATCHTOWER_EMAIL_PORT=587
WATCHTOWER_EMAIL_USER=
WATCHTOWER_EMAIL_PASSWORD=
# Traefik Let's Encrypt Email
ACME_EMAIL=admin@localhost
EOF
chmod 600 "$homelab_dir/.env"
log "✅ Environment file created with secure permissions"
}
# --------------------------------------------
# SERVICE SELECTION
# --------------------------------------------
select_services() {
echo -e "\n📺 CORE MEDIA TOOLS"
echo "1) Sonarr 2) Radarr 3) Lidarr 4) Readarr"
echo "5) Bazarr 6) Prowlarr 7) Tdarr"
echo -e "\n⬇️ DOWNLOAD CLIENTS"
echo "8) NZBGet 9) SABnzbd 10) Transmission 11) qBittorrent"
echo -e "\n🎞️ MEDIA SERVERS"
echo "12) Plex 13) Jellyfin 14) Jellystat 15) Emby"
echo -e "\n🎛️ REQUEST MANAGEMENT"
echo "16) Overseerr 17) Jellyseerr 18) Ombi"
echo -e "\n🔒 NETWORK & SECURITY"
echo "19) Traefik 20) Nginx Proxy Manager 21) Authelia"
echo -e "\n📈 MONITORING"
echo "22) Portainer 23) Watchtower 24) Uptime Kuma"
echo -e "\n📝 Select services (space-separated numbers, or 'all' for everything): "
read -a service_choices
declare -A SERVICE_MAP=(
[1]="sonarr" [2]="radarr" [3]="lidarr" [4]="readarr"
[5]="bazarr" [6]="prowlarr" [7]="tdarr" [8]="nzbget"
[9]="sabnzbd" [10]="transmission" [11]="qbittorrent"
[12]="plex" [13]="jellyfin" [14]="jellystat" [15]="emby"
[16]="overseerr" [17]="jellyseerr" [18]="ombi"
[19]="traefik" [20]="nginx-proxy-manager" [21]="authelia"
[22]="portainer" [23]="watchtower" [24]="uptime-kuma"
)
SERVICES=()
if [[ "${service_choices[0]}" == "all" ]]; then
SERVICES=($(printf '%s\n' "${SERVICE_MAP[@]}" | sort))
log "🎯 Selected all services"
else
for choice in "${service_choices[@]}"; do
[[ -n "${SERVICE_MAP[$choice]}" ]] && SERVICES+=("${SERVICE_MAP[$choice]}")
done
fi
if [[ ${#SERVICES[@]} -eq 0 ]]; then
error_exit "No valid services selected."
fi
log "✅ Selected services: ${SERVICES[*]}"
# Check for service dependencies and conflicts
check_service_dependencies
check_service_conflicts
}
# --------------------------------------------
# DEPENDENCY AND CONFLICT CHECKING
# --------------------------------------------
check_service_dependencies() {
# Source service definitions for dependency resolution
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
# Resolve dependencies
local all_services=($(resolve_dependencies "${SERVICES[@]}"))
local deps_added=()
for service in "${all_services[@]}"; do
if [[ ! " ${SERVICES[*]} " =~ " ${service} " ]]; then
deps_added+=("$service")
fi
done
if [[ ${#deps_added[@]} -gt 0 ]]; then
log "📦 Adding dependencies: ${deps_added[*]}"
SERVICES=("${all_services[@]}")
fi
fi
# Check if any *arr services are selected without Prowlarr
local arr_services=(sonarr radarr lidarr readarr)
local has_arr=false
for arr in "${arr_services[@]}"; do
if [[ "${SERVICES[*]}" =~ $arr ]]; then
has_arr=true
break
fi
done
if [[ $has_arr == true ]] && [[ ! "${SERVICES[*]}" =~ prowlarr ]]; then
echo -e "\n💡 Recommendation: You selected *arr services but not Prowlarr."
echo "Prowlarr manages indexers for all *arr applications."
echo -e "Add Prowlarr? [Y/n]: "
read -r add_prowlarr
if [[ ! "$add_prowlarr" =~ ^[Nn]$ ]]; then
SERVICES+=("prowlarr")
log "✅ Added Prowlarr"
fi
fi
}
check_service_conflicts() {
local warnings=()
# Check for multiple media servers
local media_servers=(plex jellyfin emby)
local selected_media_servers=()
for server in "${media_servers[@]}"; do
if [[ "${SERVICES[*]}" =~ $server ]]; then
selected_media_servers+=("$server")
fi
done
if [[ ${#selected_media_servers[@]} -gt 1 ]]; then
warnings+=("Multiple media servers selected: ${selected_media_servers[*]}")
fi
# Check for multiple reverse proxies
local reverse_proxies=(traefik nginx-proxy-manager)
local selected_proxies=()
for proxy in "${reverse_proxies[@]}"; do
if [[ "${SERVICES[*]}" =~ $proxy ]]; then
selected_proxies+=("$proxy")
fi
done
if [[ ${#selected_proxies[@]} -gt 1 ]]; then
warnings+=("Multiple reverse proxies selected: ${selected_proxies[*]} (may conflict on ports 80/443)")
fi
# Display warnings if any
if [[ ${#warnings[@]} -gt 0 ]]; then
log "⚠️ Configuration warnings:"
for warning in "${warnings[@]}"; do
log "$warning"
done
echo -e "\n⚠️ Continue with this configuration? [y/N]: "
read -r continue_choice
if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then
log "🚫 Installation cancelled by user"
exit 0
fi
fi
}
# --------------------------------------------
# DOCKER COMPOSE FILE GENERATION
# --------------------------------------------
generate_docker_compose() {
local HOMELAB_DIR="$HOME/homelab"
mkdir -p "$HOMELAB_DIR"
cd "$HOMELAB_DIR"
if [[ -f docker-compose.yml ]]; then
local BACKUP_FILE="docker-compose.yml.bak.$(date +%Y%m%d%H%M%S)"
log "📝 Backing up existing compose file to $BACKUP_FILE"
mv docker-compose.yml "$BACKUP_FILE"
fi
log "📝 Generating Docker Compose configuration..."
create_env_file "$HOMELAB_DIR"
# Source the service definitions
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
# Export variables for service definitions
export PUID PGID TIMEZONE MEDIA_DIR APPDATA_DIR
# Generate complete compose file with all services
generate_complete_compose "${SERVICES[@]}"
track_step "compose_generated"
# Create service-specific configurations
create_service_configs "${SERVICES[@]}"
log "✅ Generated Docker Compose with ${#SERVICES[@]} services"
else
error_exit "Service definitions file not found: $SCRIPT_DIR/hops_service_definitions.sh"
fi
# Create networks if they don't exist
create_docker_networks
}
# --------------------------------------------
# NETWORK CREATION
# --------------------------------------------
create_docker_networks() {
log "🌐 Creating Docker networks..."
# Create traefik network if it doesn't exist
if ! docker network ls --format "{{.Name}}" | grep -q "^traefik$"; then
if docker network create traefik 2>/dev/null; then
log "✅ Created traefik network"
else
log "⚠️ Could not create traefik network (may already exist)"
fi
fi
}
# --------------------------------------------
# ENHANCED DEPLOYMENT WITH ROLLBACK
# --------------------------------------------
deploy_services() {
log "🚀 Starting deployment..."
# Set up error trap
trap 'error_exit_with_rollback "Deployment failed at step: ${BASH_COMMAND}"' ERR
# Pre-deployment checks
log "🔍 Running pre-deployment validation..."
if ! docker info >/dev/null 2>&1; then
error_exit_with_rollback "Docker daemon is not running or accessible"
fi
if ! docker compose config >/dev/null 2>&1; then
error_exit_with_rollback "Generated docker-compose.yml is invalid"
fi
# Create required directories
log "📁 Creating required directories..."
for svc in "${SERVICES[@]}"; do
mkdir -p "${APPDATA_DIR}/${svc}"
chown -R "$PUID:$PGID" "${APPDATA_DIR}/${svc}" 2>/dev/null || true
done
track_step "directories_created"
# Pull images with retry logic
log "📥 Pulling container images..."
local PULL_RETRIES=3
for attempt in $(seq 1 $PULL_RETRIES); do
if docker compose pull 2>&1 | tee -a "$LOG_FILE"; then
track_step "images_pulled"
break
elif [[ $attempt -eq $PULL_RETRIES ]]; then
error_exit_with_rollback "Failed to pull images after $PULL_RETRIES attempts"
else
log "⚠️ Pull attempt $attempt failed, retrying in 10 seconds..."
sleep 10
fi
done
# Start containers
log "🔄 Starting containers..."
if docker compose up -d 2>&1 | tee -a "$LOG_FILE"; then
track_step "containers_started"
else
log "❌ Some containers failed to start. Checking status..."
docker compose ps
error_exit_with_rollback "Container startup failed"
fi
# Clear trap on success
trap - ERR
}
# --------------------------------------------
# ENHANCED SERVICE VERIFICATION
# --------------------------------------------
verify_service_health() {
local service_name="$1"
local max_wait=300 # 5 minutes
local interval=10
log "🔍 Waiting for $service_name to be healthy..."
for ((i=0; i<max_wait; i+=interval)); do
local health=$(docker inspect --format='{{.State.Health.Status}}' "$service_name" 2>/dev/null || echo "none")
case "$health" in
"healthy")
log "$service_name is healthy"
return 0
;;
"starting")
log "$service_name is starting... (${i}s elapsed)"
;;
"unhealthy")
log "$service_name is unhealthy"
return 1
;;
"none")
# No health check defined, check if container is running
local state=$(docker inspect --format='{{.State.Status}}' "$service_name" 2>/dev/null || echo "unknown")
if [[ "$state" == "running" ]]; then
log "$service_name is running (no health check)"
return 0
fi
;;
esac
sleep "$interval"
done
log "⚠️ $service_name health check timed out"
return 1
}
verify_services() {
log "🩺 Verifying service health..."
local FAILED_SERVICES=()
for svc in "${SERVICES[@]}"; do
if docker ps --format "{{.Names}}" | grep -qi "^${svc}$"; then
if ! verify_service_health "$svc"; then
FAILED_SERVICES+=("$svc")
fi
else
log "$svc container not found"
FAILED_SERVICES+=("$svc")
fi
done
if [[ ${#FAILED_SERVICES[@]} -gt 0 ]]; then
log "⚠️ Services requiring attention:"
for svc in "${FAILED_SERVICES[@]}"; do
log "$svc - Check logs: docker logs $svc"
done
else
log "✅ All services are healthy"
fi
}
# --------------------------------------------
# SECURITY SETUP
# --------------------------------------------
setup_security() {
log "🔒 Applying security hardening..."
# Secure sensitive files
find "$APPDATA_DIR" -name "*.env" -exec chmod 600 {} \; 2>/dev/null || true
find "$APPDATA_DIR" -name "*.key" -exec chmod 600 {} \; 2>/dev/null || true
find "$APPDATA_DIR" -name "*.pem" -exec chmod 600 {} \; 2>/dev/null || true
# Set secure permissions on homelab directory
chmod 750 "$HOME/homelab"
log "✅ Security hardening applied"
}
setup_firewall() {
if command -v ufw &>/dev/null; then
log "🔥 Configuring UFW firewall..."
# Don't reset if already configured
if ! ufw status | grep -q "Status: active"; then
ufw --force reset >/dev/null 2>&1
ufw default deny incoming >/dev/null 2>&1
ufw default allow outgoing >/dev/null 2>&1
# Allow SSH
ufw allow ssh >/dev/null 2>&1
fi
# Allow service ports based on selection
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
for svc in "${SERVICES[@]}"; do
local ports=$(get_service_ports "$svc")
for port in $ports; do
# Skip UDP ports and handle TCP/UDP notation
if [[ "$port" =~ /udp$ ]]; then
local port_num="${port%/udp}"
ufw allow "$port_num/udp" comment "$svc" >/dev/null 2>&1
else
local port_num="${port%/tcp}"
ufw allow "$port_num/tcp" comment "$svc" >/dev/null 2>&1
fi
done
done
fi
ufw --force enable >/dev/null 2>&1
log "✅ Firewall configured"
else
log "️ UFW not available, skipping firewall configuration"
fi
}
# --------------------------------------------
# MAIN INSTALLATION FLOW
# --------------------------------------------
check_system_requirements
detect_os
check_required_packages
collect_user_configuration
select_services
check_all_ports "${SERVICES[@]}"
# Install dependencies
log "📦 Installing prerequisites..."
if ! apt-get update &>/dev/null; then
error_exit "Failed to update package lists. Check your internet connection."
fi
local REQUIRED_PACKAGES="ca-certificates curl gnupg lsb-release lsof ufw fail2ban openssl apache2-utils"
if ! apt-get install -y $REQUIRED_PACKAGES 2>&1 | tee -a "$LOG_FILE"; then
error_exit "Failed to install required packages."
fi
# Install Docker if not present
if ! command -v docker &>/dev/null; then
log "🐳 Installing Docker..."
if ! curl -fsSL https://get.docker.com | sh 2>&1 | tee -a "$LOG_FILE"; then
error_exit "Failed to install Docker."
fi
# Add user to docker group
if [[ -n "$SUDO_USER" ]]; then
usermod -aG docker "$SUDO_USER"
log "✅ Added $SUDO_USER to docker group (restart session to take effect)"
fi
else
log "✅ Docker already installed ($(docker --version))"
fi
check_docker_compose_version
# Ensure Docker daemon is running
if ! systemctl is-active --quiet docker; then
log "🔄 Starting Docker daemon..."
systemctl start docker || error_exit "Failed to start Docker daemon"
systemctl enable docker || log "⚠️ Could not enable Docker service"
fi
setup_firewall
generate_docker_compose
deploy_services
setup_security
verify_services
# --------------------------------------------
# FINAL SUMMARY
# --------------------------------------------
echo -e "\n🎉 HOPS Enhanced Deployment Complete!"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "📋 Deployment Summary:"
echo -e "\n📂 Configuration:"
echo " • Homelab Directory: $HOME/homelab"
echo " • Application Data: $APPDATA_DIR"
echo " • Media Directory: $MEDIA_DIR"
echo " • User/Group: $RUNNING_USER ($PUID:$PGID)"
echo " • Timezone: $TIMEZONE"
echo " • Log File: $LOG_FILE"
echo -e "\n🔐 Security:"
echo " • Generated secure passwords (see .env file)"
echo " • Firewall configured with service-specific rules"
echo " • File permissions hardened"
echo -e "\n📱 Deployed Services:"
local service_count=0
for svc in "${SERVICES[@]}"; do
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
local ports=$(get_service_ports "$svc")
local main_port=$(echo $ports | cut -d' ' -f1)
if [[ -n "$main_port" ]]; then
echo "$svc: http://$(hostname -I | awk '{print $1}'):$main_port"
((service_count++))
fi
fi
done
echo -e "\n🔧 Management Commands:"
echo " • View all logs: docker compose logs -f"
echo " • View service logs: docker compose logs -f [service]"
echo " • Restart service: docker compose restart [service]"
echo " • Stop services: docker compose down"
echo " • Start services: docker compose up -d"
echo " • Update services: docker compose pull && docker compose up -d"
echo -e "\n📚 Next Steps:"
echo " 1. Access services using the URLs above"
echo " 2. Change default passwords from .env file"
echo " 3. Configure services according to your needs"
echo " 4. Set up your media library paths"
echo -e "\n📋 Logs and troubleshooting: $LOG_FILE"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "🎉 HOPS Enhanced deployment completed successfully!"
return 0
}
+679
View File
@@ -0,0 +1,679 @@
#!/bin/bash
# HOPS - Homelab Orchestration Provisioning Script
# Primary Management Script
# Version: 3.1.0
# Exit on any error
set -e
# Script version and metadata
readonly SCRIPT_VERSION="3.1.0"
readonly SCRIPT_NAME="HOPS"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default script locations
readonly INSTALLER_SCRIPT="$SCRIPT_DIR/hops_installer_enhanced.sh"
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/hops_uninstaller_fixed.sh"
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/hops_service_definitions.sh"
# Color codes for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly PURPLE='\033[0;35m'
readonly CYAN='\033[0;36m'
readonly WHITE='\033[1;37m'
readonly NC='\033[0m' # No Color
# Logging setup
readonly LOG_DIR="/var/log/hops"
readonly LOG_FILE="$LOG_DIR/hops-main-$(date +%Y%m%d-%H%M%S).log"
# Initialize logging
init_logging() {
if [[ $EUID -eq 0 ]]; then
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
fi
}
# Logging function
log() {
local message="$1"
local timestamp="$(date '+%Y-%m-%d %T')"
if [[ -w "$LOG_FILE" ]]; then
echo "$timestamp - $message" >> "$LOG_FILE"
fi
echo -e "$message"
}
# Error handling
error_exit() {
log "${RED}❌ ERROR: $1${NC}"
exit 1
}
# Warning function
warning() {
log "${YELLOW}⚠️ WARNING: $1${NC}"
}
# Success function
success() {
log "${GREEN}$1${NC}"
}
# Info function
info() {
log "${BLUE}$1${NC}"
}
# Clear screen and show header
show_header() {
clear
cat << "EOF"
_ _ ____ ____ ____
| | | || _ \| _ \/ ___|
| |__| || |_) | |_) \___ \
| __ || __/| __/ ___) |
|_| |_||_| |_| |____/
EOF
echo -e "${CYAN}🚀 Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}${NC}"
echo -e "${WHITE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root or with sudo."
fi
}
# Validate script dependencies
check_dependencies() {
local missing_deps=()
# Check for required scripts
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
missing_deps+=("Installer script: $INSTALLER_SCRIPT")
fi
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
missing_deps+=("Uninstaller script: $UNINSTALLER_SCRIPT")
fi
if [[ ! -f "$SERVICE_DEFINITIONS" ]]; then
missing_deps+=("Service definitions: $SERVICE_DEFINITIONS")
fi
# Check for required commands
local required_commands=("curl" "wget" "systemctl")
for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
missing_deps+=("Command: $cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
error_exit "Missing dependencies:\n$(printf ' • %s\n' "${missing_deps[@]}")"
fi
}
# Check system requirements
check_system_requirements() {
info "Checking system requirements..."
# Check OS
if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then
warning "This script is designed for Ubuntu/Debian/Mint systems"
echo -e "Continue anyway? [y/N]: "
read -r continue_choice
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 0
fi
# Check minimum requirements
local ram_gb=$(free -g | awk '/^Mem:/{print $2}')
local disk_gb=$(df -BG --output=avail / | tail -n 1 | tr -d 'G')
if [[ $ram_gb -lt 2 ]]; then
warning "Low RAM detected: ${ram_gb}GB (2GB+ recommended)"
fi
if [[ $disk_gb -lt 10 ]]; then
warning "Low disk space: ${disk_gb}GB (10GB+ recommended)"
fi
success "System requirements check complete"
}
# Get HOPS installation status
get_installation_status() {
local status="not_installed"
local homelab_dirs=(
"$HOME/homelab"
"/home/*/homelab"
"/opt/homelab"
"/srv/homelab"
)
# Check for existing installation
for dir in "${homelab_dirs[@]}"; do
if [[ -f "$dir/docker-compose.yml" ]]; then
status="installed"
HOMELAB_DIR="$dir"
break
fi
done
# Check for running containers
if command -v docker &>/dev/null && docker ps --format "{{.Names}}" | grep -qE "(sonarr|radarr|jellyfin|plex|portainer)"; then
if [[ "$status" == "not_installed" ]]; then
status="partial"
else
status="running"
fi
fi
echo "$status"
}
# Show installation status
show_status() {
local status=$(get_installation_status)
echo -e "${WHITE}📊 Current Status:${NC}"
case "$status" in
"not_installed")
echo -e " ${RED}● Not Installed${NC}"
;;
"installed")
echo -e " ${YELLOW}● Installed (stopped)${NC}"
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
;;
"running")
echo -e " ${GREEN}● Running${NC}"
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
;;
"partial")
echo -e " ${YELLOW}● Partial Installation${NC}"
;;
esac
echo
}
# Run installer
run_installer() {
info "Launching HOPS installer..."
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
error_exit "Installer script not found: $INSTALLER_SCRIPT"
fi
# Source the installer function and run it
if source "$INSTALLER_SCRIPT" && install_hops; then
success "Installation completed successfully!"
else
error_exit "Installation failed. Check logs for details."
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Run uninstaller
run_uninstaller() {
info "Launching HOPS uninstaller..."
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
error_exit "Uninstaller script not found: $UNINSTALLER_SCRIPT"
fi
# Source the uninstaller function and run it
if source "$UNINSTALLER_SCRIPT" && uninstall_hops; then
success "Uninstallation completed successfully!"
else
error_exit "Uninstallation failed. Check logs for details."
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show service status
show_service_status() {
show_header
echo -e "${WHITE}🔍 Service Status Check${NC}\n"
if ! command -v docker &>/dev/null; then
warning "Docker is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}📊 Docker Service Status:${NC}"
if systemctl is-active --quiet docker; then
echo -e " ${GREEN}● Docker daemon: Running${NC}"
else
echo -e " ${RED}● Docker daemon: Stopped${NC}"
fi
echo -e "\n${BLUE}📦 Container Status:${NC}"
# Source service definitions to get port info
if [[ -f "$SERVICE_DEFINITIONS" ]]; then
source "$SERVICE_DEFINITIONS"
fi
# Known HOPS services with their ports
local services=(
"sonarr:8989" "radarr:7878" "lidarr:8686" "readarr:8787"
"bazarr:6767" "prowlarr:9696" "jellyfin:8096" "plex:32400"
"overseerr:5055" "jellyseerr:5056" "portainer:9000"
"traefik:8080" "nginx-proxy-manager:81" "qbittorrent:8082"
"transmission:9091" "nzbget:6789" "sabnzbd:8080"
"uptime-kuma:3001" "jellystat:3000"
)
local running_count=0
local total_count=0
for service_info in "${services[@]}"; do
local service_name="${service_info%:*}"
local service_port="${service_info#*:}"
if docker ps --format "{{.Names}}" | grep -q "^${service_name}$"; then
((total_count++))
local status_symbol="${GREEN}${NC}"
local status_text="Running"
# Check if port is accessible
if curl -sSf --max-time 2 --connect-timeout 1 "http://localhost:${service_port}" >/dev/null 2>&1; then
status_text="Running & Accessible"
((running_count++))
else
status_text="Running (starting up)"
status_symbol="${YELLOW}${NC}"
fi
printf " %s %-20s %s (:%s)\n" "$status_symbol" "$service_name" "$status_text" "$service_port"
elif docker ps -a --format "{{.Names}}" | grep -q "^${service_name}$"; then
((total_count++))
printf " %s %-20s %s\n" "${RED}${NC}" "$service_name" "Stopped"
fi
done
if [[ $total_count -eq 0 ]]; then
echo -e " ${YELLOW}No HOPS services found${NC}"
else
echo -e "\n${WHITE}📈 Summary: ${running_count}/${total_count} services running and accessible${NC}"
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Manage services (start/stop/restart)
manage_services() {
show_header
echo -e "${WHITE}🎛️ Service Management${NC}\n"
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
if [[ -z "$HOMELAB_DIR" ]]; then
error_exit "Cannot locate homelab directory with docker-compose.yml"
fi
echo -e "${BLUE}Available actions:${NC}"
echo -e " 1) Start all services"
echo -e " 2) Stop all services"
echo -e " 3) Restart all services"
echo -e " 4) View logs (recent)"
echo -e " 5) View logs (follow)"
echo -e " 6) Update services"
echo -e " 7) Restart individual service"
echo -e " 8) Back to main menu"
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
read -r choice
cd "$HOMELAB_DIR"
case $choice in
1)
info "Starting all services..."
if docker compose up -d; then
success "Services started"
else
warning "Some services may have failed to start"
fi
;;
2)
info "Stopping all services..."
if docker compose down; then
success "Services stopped"
else
warning "Some services may not have stopped cleanly"
fi
;;
3)
info "Restarting all services..."
if docker compose restart; then
success "Services restarted"
else
warning "Some services may have failed to restart"
fi
;;
4)
info "Showing recent logs..."
docker compose logs --tail=100
;;
5)
info "Following logs (Ctrl+C to exit)..."
docker compose logs -f --tail=50
;;
6)
info "Updating services..."
if docker compose pull && docker compose up -d; then
success "Services updated"
else
warning "Update may have failed"
fi
;;
7)
echo -e "\n${WHITE}Available services:${NC}"
docker compose ps --services | nl -w2 -s') '
echo -e "\n${WHITE}Enter service name to restart: ${NC}"
read -r service_name
if [[ -n "$service_name" ]]; then
info "Restarting $service_name..."
if docker compose restart "$service_name"; then
success "$service_name restarted"
else
warning "Failed to restart $service_name"
fi
fi
;;
8)
return
;;
*)
warning "Invalid option"
;;
esac
echo -e "\n${WHITE}Press Enter to continue...${NC}"
read -r
}
# Show quick access URLs
show_access_info() {
show_header
echo -e "${WHITE}🌐 Service Access Information${NC}\n"
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}📱 Access your services at:${NC}"
# Get local IP
local local_ip=$(hostname -I | awk '{print $1}')
# Service URLs with paths
local services=(
"Sonarr:8989:/sonarr"
"Radarr:7878:/radarr"
"Lidarr:8686:/lidarr"
"Readarr:8787:/readarr"
"Bazarr:6767:"
"Prowlarr:9696:"
"Jellyfin:8096:"
"Plex:32400:/web"
"Overseerr:5055:"
"Jellyseerr:5056:"
"Portainer:9000:"
"Traefik:8080:"
"NPM:81:"
"qBittorrent:8082:"
"Transmission:9091:"
"NZBGet:6789:"
"SABnzbd:8080:"
"Uptime-Kuma:3001:"
"Jellystat:3000:"
)
local active_services=0
for service_info in "${services[@]}"; do
local service_name="${service_info%%:*}"
local service_port="${service_info#*:}"
local service_path="${service_port#*:}"
service_port="${service_port%:*}"
if docker ps --format "{{.Names}}" | grep -qi "${service_name,,}"; then
local url="http://${local_ip}:${service_port}${service_path}"
printf " ${GREEN}${NC} %-15s %s\n" "$service_name" "$url"
((active_services++))
fi
done
if [[ $active_services -eq 0 ]]; then
echo -e " ${YELLOW}No services currently running${NC}"
fi
echo -e "\n${YELLOW}💡 Tips:${NC}"
echo -e " • Bookmark these URLs for easy access"
echo -e " • Default credentials are in the .env file"
echo -e " • Change default passwords after first login"
echo -e " • Some services may take a few minutes to fully start"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show logs
show_logs() {
show_header
echo -e "${WHITE}📋 HOPS Logs${NC}\n"
if [[ ! -d "$LOG_DIR" ]]; then
warning "No log directory found"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}Available log files:${NC}"
local log_files=($(find "$LOG_DIR" -name "*.log" -type f | sort -r))
if [[ ${#log_files[@]} -eq 0 ]]; then
warning "No log files found"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
local count=1
for log_file in "${log_files[@]}"; do
local basename_log=$(basename "$log_file")
local size=$(du -h "$log_file" | cut -f1)
local date=$(stat -c %y "$log_file" | cut -d' ' -f1)
printf " %d) %-40s (%s, %s)\n" "$count" "$basename_log" "$size" "$date"
((count++))
done
echo -e "\n${WHITE}Select a log file to view [1-${#log_files[@]}] or 0 to go back: ${NC}"
read -r choice
if [[ "$choice" -eq 0 ]]; then
return
elif [[ "$choice" -gt 0 && "$choice" -le ${#log_files[@]} ]]; then
local selected_log="${log_files[$((choice-1))]}"
echo -e "\n${BLUE}Showing last 50 lines of $(basename "$selected_log"):${NC}\n"
tail -50 "$selected_log"
else
warning "Invalid selection"
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show help information
show_help() {
show_header
echo -e "${WHITE}📚 HOPS Help & Documentation${NC}\n"
echo -e "${BLUE}🎯 What is HOPS?${NC}"
echo -e "HOPS (Homelab Orchestration Provisioning Script) is an automated installer"
echo -e "for popular homelab applications including media servers, download clients,"
echo -e "and monitoring tools.\n"
echo -e "${BLUE}🚀 Quick Start:${NC}"
echo -e " 1. Run this script as root/sudo"
echo -e " 2. Choose 'Install HOPS' from the menu"
echo -e " 3. Configure directories and timezone"
echo -e " 4. Select your desired services"
echo -e " 5. Wait for installation to complete"
echo -e " 6. Access services via the provided URLs\n"
echo -e "${BLUE}📱 Supported Services:${NC}"
echo -e " • Media Management: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr"
echo -e " • Download Clients: qBittorrent, Transmission, NZBGet, SABnzbd"
echo -e " • Media Servers: Jellyfin, Plex, Emby, Jellystat"
echo -e " • Request Management: Overseerr, Jellyseerr, Ombi"
echo -e " • Reverse Proxy: Traefik, Nginx Proxy Manager"
echo -e " • Monitoring: Portainer, Uptime Kuma, Watchtower\n"
echo -e "${BLUE}🔧 Requirements:${NC}"
echo -e " • Ubuntu/Debian/Mint Linux"
echo -e " • 2GB+ RAM (4GB+ recommended)"
echo -e " • 10GB+ free disk space"
echo -e " • Root/sudo access"
echo -e " • Internet connection\n"
echo -e "${BLUE}📁 Default Locations:${NC}"
echo -e " • Homelab directory: ~/homelab/"
echo -e " • App configurations: /opt/appdata/"
echo -e " • Media storage: /mnt/media/"
echo -e " • Logs: /var/log/hops/\n"
echo -e "${BLUE}🆘 Troubleshooting:${NC}"
echo -e " • Check logs in the 'View Logs' menu"
echo -e " • Verify Docker is running: systemctl status docker"
echo -e " • Check container status: docker ps"
echo -e " • View service logs: docker logs [service-name]"
echo -e " • Restart services: docker compose restart [service-name]\n"
echo -e "${BLUE}🔐 Security Notes:${NC}"
echo -e " • Change default passwords in .env file after installation"
echo -e " • Configure firewall rules as needed"
echo -e " • Regularly update services using the management menu\n"
echo -e "${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Main menu
show_main_menu() {
local status=$(get_installation_status)
echo -e "${WHITE}🎛️ Main Menu:${NC}"
echo -e " 1) Install HOPS"
if [[ "$status" != "not_installed" ]]; then
echo -e " 2) Uninstall HOPS"
echo -e " 3) Manage Services"
echo -e " 4) Service Status"
echo -e " 5) Access Information"
else
echo -e " 2) Uninstall HOPS ${YELLOW}(not installed)${NC}"
echo -e " 3) Manage Services ${YELLOW}(not installed)${NC}"
echo -e " 4) Service Status ${YELLOW}(not installed)${NC}"
echo -e " 5) Access Information ${YELLOW}(not installed)${NC}"
fi
echo -e " 6) View Logs"
echo -e " 7) Help & Documentation"
echo -e " 8) Exit"
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
}
# Main program loop
main() {
init_logging
check_root
check_dependencies
while true; do
show_header
show_status
show_main_menu
read -r choice
case $choice in
1)
check_system_requirements
run_installer
;;
2)
run_uninstaller
;;
3)
manage_services
;;
4)
show_service_status
;;
5)
show_access_info
;;
6)
show_logs
;;
7)
show_help
;;
8)
info "Thank you for using HOPS!"
exit 0
;;
*)
warning "Invalid option. Please select 1-8."
sleep 2
;;
esac
done
}
# Script entry point
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+1285
View File
File diff suppressed because it is too large Load Diff
+560
View File
@@ -0,0 +1,560 @@
#!/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="3.1.0"
# --------------------------------------------
# LOGGING SETUP
# --------------------------------------------
local LOG_DIR="/var/log/hops"
local LOG_FILE="$LOG_DIR/homelab-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/homelab"
"/home/*/homelab"
"/opt/homelab"
"/srv/homelab"
)
# Try to find from running user's home first
if [[ -n "$SUDO_USER" ]]; then
local user_home=$(eval echo "~$SUDO_USER")
POSSIBLE_DIRS=("$user_home/homelab" "${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
}
Executable
+679
View File
@@ -0,0 +1,679 @@
#!/bin/bash
# HOPS - Homelab Orchestration Provisioning Script
# Primary Management Script
# Version: 3.1.0
# Exit on any error
set -e
# Script version and metadata
readonly SCRIPT_VERSION="3.1.0"
readonly SCRIPT_NAME="HOPS"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default script locations
readonly INSTALLER_SCRIPT="$SCRIPT_DIR/hops_installer_enhanced.sh"
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/hops_uninstaller_fixed.sh"
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/hops_service_definitions.sh"
# Color codes for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly PURPLE='\033[0;35m'
readonly CYAN='\033[0;36m'
readonly WHITE='\033[1;37m'
readonly NC='\033[0m' # No Color
# Logging setup
readonly LOG_DIR="/var/log/hops"
readonly LOG_FILE="$LOG_DIR/hops-main-$(date +%Y%m%d-%H%M%S).log"
# Initialize logging
init_logging() {
if [[ $EUID -eq 0 ]]; then
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
fi
}
# Logging function
log() {
local message="$1"
local timestamp="$(date '+%Y-%m-%d %T')"
if [[ -w "$LOG_FILE" ]]; then
echo "$timestamp - $message" >> "$LOG_FILE"
fi
echo -e "$message"
}
# Error handling
error_exit() {
log "${RED}❌ ERROR: $1${NC}"
exit 1
}
# Warning function
warning() {
log "${YELLOW}⚠️ WARNING: $1${NC}"
}
# Success function
success() {
log "${GREEN}$1${NC}"
}
# Info function
info() {
log "${BLUE}$1${NC}"
}
# Clear screen and show header
show_header() {
clear
cat << "EOF"
_ _ ____ ____ ____
| | | || _ \| _ \/ ___|
| |__| || |_) | |_) \___ \
| __ || __/| __/ ___) |
|_| |_||_| |_| |____/
EOF
echo -e "${CYAN}🚀 Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}${NC}"
echo -e "${WHITE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root or with sudo."
fi
}
# Validate script dependencies
check_dependencies() {
local missing_deps=()
# Check for required scripts
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
missing_deps+=("Installer script: $INSTALLER_SCRIPT")
fi
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
missing_deps+=("Uninstaller script: $UNINSTALLER_SCRIPT")
fi
if [[ ! -f "$SERVICE_DEFINITIONS" ]]; then
missing_deps+=("Service definitions: $SERVICE_DEFINITIONS")
fi
# Check for required commands
local required_commands=("curl" "wget" "systemctl")
for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
missing_deps+=("Command: $cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
error_exit "Missing dependencies:\n$(printf ' • %s\n' "${missing_deps[@]}")"
fi
}
# Check system requirements
check_system_requirements() {
info "Checking system requirements..."
# Check OS
if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then
warning "This script is designed for Ubuntu/Debian/Mint systems"
echo -e "Continue anyway? [y/N]: "
read -r continue_choice
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 0
fi
# Check minimum requirements
local ram_gb=$(free -g | awk '/^Mem:/{print $2}')
local disk_gb=$(df -BG --output=avail / | tail -n 1 | tr -d 'G')
if [[ $ram_gb -lt 2 ]]; then
warning "Low RAM detected: ${ram_gb}GB (2GB+ recommended)"
fi
if [[ $disk_gb -lt 10 ]]; then
warning "Low disk space: ${disk_gb}GB (10GB+ recommended)"
fi
success "System requirements check complete"
}
# Get HOPS installation status
get_installation_status() {
local status="not_installed"
local homelab_dirs=(
"$HOME/homelab"
"/home/*/homelab"
"/opt/homelab"
"/srv/homelab"
)
# Check for existing installation
for dir in "${homelab_dirs[@]}"; do
if [[ -f "$dir/docker-compose.yml" ]]; then
status="installed"
HOMELAB_DIR="$dir"
break
fi
done
# Check for running containers
if command -v docker &>/dev/null && docker ps --format "{{.Names}}" | grep -qE "(sonarr|radarr|jellyfin|plex|portainer)"; then
if [[ "$status" == "not_installed" ]]; then
status="partial"
else
status="running"
fi
fi
echo "$status"
}
# Show installation status
show_status() {
local status=$(get_installation_status)
echo -e "${WHITE}📊 Current Status:${NC}"
case "$status" in
"not_installed")
echo -e " ${RED}● Not Installed${NC}"
;;
"installed")
echo -e " ${YELLOW}● Installed (stopped)${NC}"
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
;;
"running")
echo -e " ${GREEN}● Running${NC}"
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
;;
"partial")
echo -e " ${YELLOW}● Partial Installation${NC}"
;;
esac
echo
}
# Run installer
run_installer() {
info "Launching HOPS installer..."
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
error_exit "Installer script not found: $INSTALLER_SCRIPT"
fi
# Source the installer function and run it
if source "$INSTALLER_SCRIPT" && install_hops; then
success "Installation completed successfully!"
else
error_exit "Installation failed. Check logs for details."
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Run uninstaller
run_uninstaller() {
info "Launching HOPS uninstaller..."
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
error_exit "Uninstaller script not found: $UNINSTALLER_SCRIPT"
fi
# Source the uninstaller function and run it
if source "$UNINSTALLER_SCRIPT" && uninstall_hops; then
success "Uninstallation completed successfully!"
else
error_exit "Uninstallation failed. Check logs for details."
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show service status
show_service_status() {
show_header
echo -e "${WHITE}🔍 Service Status Check${NC}\n"
if ! command -v docker &>/dev/null; then
warning "Docker is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}📊 Docker Service Status:${NC}"
if systemctl is-active --quiet docker; then
echo -e " ${GREEN}● Docker daemon: Running${NC}"
else
echo -e " ${RED}● Docker daemon: Stopped${NC}"
fi
echo -e "\n${BLUE}📦 Container Status:${NC}"
# Source service definitions to get port info
if [[ -f "$SERVICE_DEFINITIONS" ]]; then
source "$SERVICE_DEFINITIONS"
fi
# Known HOPS services with their ports
local services=(
"sonarr:8989" "radarr:7878" "lidarr:8686" "readarr:8787"
"bazarr:6767" "prowlarr:9696" "jellyfin:8096" "plex:32400"
"overseerr:5055" "jellyseerr:5056" "portainer:9000"
"traefik:8080" "nginx-proxy-manager:81" "qbittorrent:8082"
"transmission:9091" "nzbget:6789" "sabnzbd:8080"
"uptime-kuma:3001" "jellystat:3000"
)
local running_count=0
local total_count=0
for service_info in "${services[@]}"; do
local service_name="${service_info%:*}"
local service_port="${service_info#*:}"
if docker ps --format "{{.Names}}" | grep -q "^${service_name}$"; then
((total_count++))
local status_symbol="${GREEN}${NC}"
local status_text="Running"
# Check if port is accessible
if curl -sSf --max-time 2 --connect-timeout 1 "http://localhost:${service_port}" >/dev/null 2>&1; then
status_text="Running & Accessible"
((running_count++))
else
status_text="Running (starting up)"
status_symbol="${YELLOW}${NC}"
fi
printf " %s %-20s %s (:%s)\n" "$status_symbol" "$service_name" "$status_text" "$service_port"
elif docker ps -a --format "{{.Names}}" | grep -q "^${service_name}$"; then
((total_count++))
printf " %s %-20s %s\n" "${RED}${NC}" "$service_name" "Stopped"
fi
done
if [[ $total_count -eq 0 ]]; then
echo -e " ${YELLOW}No HOPS services found${NC}"
else
echo -e "\n${WHITE}📈 Summary: ${running_count}/${total_count} services running and accessible${NC}"
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Manage services (start/stop/restart)
manage_services() {
show_header
echo -e "${WHITE}🎛️ Service Management${NC}\n"
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
if [[ -z "$HOMELAB_DIR" ]]; then
error_exit "Cannot locate homelab directory with docker-compose.yml"
fi
echo -e "${BLUE}Available actions:${NC}"
echo -e " 1) Start all services"
echo -e " 2) Stop all services"
echo -e " 3) Restart all services"
echo -e " 4) View logs (recent)"
echo -e " 5) View logs (follow)"
echo -e " 6) Update services"
echo -e " 7) Restart individual service"
echo -e " 8) Back to main menu"
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
read -r choice
cd "$HOMELAB_DIR"
case $choice in
1)
info "Starting all services..."
if docker compose up -d; then
success "Services started"
else
warning "Some services may have failed to start"
fi
;;
2)
info "Stopping all services..."
if docker compose down; then
success "Services stopped"
else
warning "Some services may not have stopped cleanly"
fi
;;
3)
info "Restarting all services..."
if docker compose restart; then
success "Services restarted"
else
warning "Some services may have failed to restart"
fi
;;
4)
info "Showing recent logs..."
docker compose logs --tail=100
;;
5)
info "Following logs (Ctrl+C to exit)..."
docker compose logs -f --tail=50
;;
6)
info "Updating services..."
if docker compose pull && docker compose up -d; then
success "Services updated"
else
warning "Update may have failed"
fi
;;
7)
echo -e "\n${WHITE}Available services:${NC}"
docker compose ps --services | nl -w2 -s') '
echo -e "\n${WHITE}Enter service name to restart: ${NC}"
read -r service_name
if [[ -n "$service_name" ]]; then
info "Restarting $service_name..."
if docker compose restart "$service_name"; then
success "$service_name restarted"
else
warning "Failed to restart $service_name"
fi
fi
;;
8)
return
;;
*)
warning "Invalid option"
;;
esac
echo -e "\n${WHITE}Press Enter to continue...${NC}"
read -r
}
# Show quick access URLs
show_access_info() {
show_header
echo -e "${WHITE}🌐 Service Access Information${NC}\n"
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}📱 Access your services at:${NC}"
# Get local IP
local local_ip=$(hostname -I | awk '{print $1}')
# Service URLs with paths
local services=(
"Sonarr:8989:/sonarr"
"Radarr:7878:/radarr"
"Lidarr:8686:/lidarr"
"Readarr:8787:/readarr"
"Bazarr:6767:"
"Prowlarr:9696:"
"Jellyfin:8096:"
"Plex:32400:/web"
"Overseerr:5055:"
"Jellyseerr:5056:"
"Portainer:9000:"
"Traefik:8080:"
"NPM:81:"
"qBittorrent:8082:"
"Transmission:9091:"
"NZBGet:6789:"
"SABnzbd:8080:"
"Uptime-Kuma:3001:"
"Jellystat:3000:"
)
local active_services=0
for service_info in "${services[@]}"; do
local service_name="${service_info%%:*}"
local service_port="${service_info#*:}"
local service_path="${service_port#*:}"
service_port="${service_port%:*}"
if docker ps --format "{{.Names}}" | grep -qi "${service_name,,}"; then
local url="http://${local_ip}:${service_port}${service_path}"
printf " ${GREEN}${NC} %-15s %s\n" "$service_name" "$url"
((active_services++))
fi
done
if [[ $active_services -eq 0 ]]; then
echo -e " ${YELLOW}No services currently running${NC}"
fi
echo -e "\n${YELLOW}💡 Tips:${NC}"
echo -e " • Bookmark these URLs for easy access"
echo -e " • Default credentials are in the .env file"
echo -e " • Change default passwords after first login"
echo -e " • Some services may take a few minutes to fully start"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show logs
show_logs() {
show_header
echo -e "${WHITE}📋 HOPS Logs${NC}\n"
if [[ ! -d "$LOG_DIR" ]]; then
warning "No log directory found"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}Available log files:${NC}"
local log_files=($(find "$LOG_DIR" -name "*.log" -type f | sort -r))
if [[ ${#log_files[@]} -eq 0 ]]; then
warning "No log files found"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
local count=1
for log_file in "${log_files[@]}"; do
local basename_log=$(basename "$log_file")
local size=$(du -h "$log_file" | cut -f1)
local date=$(stat -c %y "$log_file" | cut -d' ' -f1)
printf " %d) %-40s (%s, %s)\n" "$count" "$basename_log" "$size" "$date"
((count++))
done
echo -e "\n${WHITE}Select a log file to view [1-${#log_files[@]}] or 0 to go back: ${NC}"
read -r choice
if [[ "$choice" -eq 0 ]]; then
return
elif [[ "$choice" -gt 0 && "$choice" -le ${#log_files[@]} ]]; then
local selected_log="${log_files[$((choice-1))]}"
echo -e "\n${BLUE}Showing last 50 lines of $(basename "$selected_log"):${NC}\n"
tail -50 "$selected_log"
else
warning "Invalid selection"
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show help information
show_help() {
show_header
echo -e "${WHITE}📚 HOPS Help & Documentation${NC}\n"
echo -e "${BLUE}🎯 What is HOPS?${NC}"
echo -e "HOPS (Homelab Orchestration Provisioning Script) is an automated installer"
echo -e "for popular homelab applications including media servers, download clients,"
echo -e "and monitoring tools.\n"
echo -e "${BLUE}🚀 Quick Start:${NC}"
echo -e " 1. Run this script as root/sudo"
echo -e " 2. Choose 'Install HOPS' from the menu"
echo -e " 3. Configure directories and timezone"
echo -e " 4. Select your desired services"
echo -e " 5. Wait for installation to complete"
echo -e " 6. Access services via the provided URLs\n"
echo -e "${BLUE}📱 Supported Services:${NC}"
echo -e " • Media Management: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr"
echo -e " • Download Clients: qBittorrent, Transmission, NZBGet, SABnzbd"
echo -e " • Media Servers: Jellyfin, Plex, Emby, Jellystat"
echo -e " • Request Management: Overseerr, Jellyseerr, Ombi"
echo -e " • Reverse Proxy: Traefik, Nginx Proxy Manager"
echo -e " • Monitoring: Portainer, Uptime Kuma, Watchtower\n"
echo -e "${BLUE}🔧 Requirements:${NC}"
echo -e " • Ubuntu/Debian/Mint Linux"
echo -e " • 2GB+ RAM (4GB+ recommended)"
echo -e " • 10GB+ free disk space"
echo -e " • Root/sudo access"
echo -e " • Internet connection\n"
echo -e "${BLUE}📁 Default Locations:${NC}"
echo -e " • Homelab directory: ~/homelab/"
echo -e " • App configurations: /opt/appdata/"
echo -e " • Media storage: /mnt/media/"
echo -e " • Logs: /var/log/hops/\n"
echo -e "${BLUE}🆘 Troubleshooting:${NC}"
echo -e " • Check logs in the 'View Logs' menu"
echo -e " • Verify Docker is running: systemctl status docker"
echo -e " • Check container status: docker ps"
echo -e " • View service logs: docker logs [service-name]"
echo -e " • Restart services: docker compose restart [service-name]\n"
echo -e "${BLUE}🔐 Security Notes:${NC}"
echo -e " • Change default passwords in .env file after installation"
echo -e " • Configure firewall rules as needed"
echo -e " • Regularly update services using the management menu\n"
echo -e "${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Main menu
show_main_menu() {
local status=$(get_installation_status)
echo -e "${WHITE}🎛️ Main Menu:${NC}"
echo -e " 1) Install HOPS"
if [[ "$status" != "not_installed" ]]; then
echo -e " 2) Uninstall HOPS"
echo -e " 3) Manage Services"
echo -e " 4) Service Status"
echo -e " 5) Access Information"
else
echo -e " 2) Uninstall HOPS ${YELLOW}(not installed)${NC}"
echo -e " 3) Manage Services ${YELLOW}(not installed)${NC}"
echo -e " 4) Service Status ${YELLOW}(not installed)${NC}"
echo -e " 5) Access Information ${YELLOW}(not installed)${NC}"
fi
echo -e " 6) View Logs"
echo -e " 7) Help & Documentation"
echo -e " 8) Exit"
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
}
# Main program loop
main() {
init_logging
check_root
check_dependencies
while true; do
show_header
show_status
show_main_menu
read -r choice
case $choice in
1)
check_system_requirements
run_installer
;;
2)
run_uninstaller
;;
3)
manage_services
;;
4)
show_service_status
;;
5)
show_access_info
;;
6)
show_logs
;;
7)
show_help
;;
8)
info "Thank you for using HOPS!"
exit 0
;;
*)
warning "Invalid option. Please select 1-8."
sleep 2
;;
esac
done
}
# Script entry point
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+924
View File
@@ -0,0 +1,924 @@
#!/bin/bash
install_hops() {
# Clear terminal at startup
clear
# Exit on any error
set -e
# Script version for update tracking
local SCRIPT_VERSION="3.1.0"
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --------------------------------------------
# LOGGING SETUP
# --------------------------------------------
local LOG_DIR="/var/log/hops"
local LOG_FILE="$LOG_DIR/homelab-setup-$(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 "❌ Installation failed. Check logs at: $LOG_FILE"
exit 1
}
# Enhanced error handling with rollback
DEPLOYMENT_STEPS_COMPLETED=()
track_step() {
DEPLOYMENT_STEPS_COMPLETED+=("$1")
log "✅ Step completed: $1"
}
rollback_deployment() {
log "🔄 Rolling back deployment..."
for step in "${DEPLOYMENT_STEPS_COMPLETED[@]}"; do
case "$step" in
"containers_started")
log "🛑 Stopping containers..."
docker compose down --timeout 30 2>/dev/null || true
;;
"images_pulled")
log "🗑️ Removing pulled images..."
docker compose down --rmi all 2>/dev/null || true
;;
"directories_created")
log "📁 Cleaning up directories..."
[[ -n "$APPDATA_DIR" ]] && rm -rf "$APPDATA_DIR" 2>/dev/null || true
;;
"compose_generated")
log "📝 Removing compose file..."
[[ -f "docker-compose.yml" ]] && rm -f docker-compose.yml
;;
esac
done
log "🔄 Rollback completed"
}
error_exit_with_rollback() {
log "❌ ERROR: $1"
rollback_deployment
log "❌ Installation failed and rolled back. Check logs at: $LOG_FILE"
exit 1
}
# --------------------------------------------
# HEADER
# --------------------------------------------
cat << "EOF"
_ _ ____ ____ ____
| | | || _ \| _ \/ ___|
| |__| || |_) | |_) \___ \
| __ || __/| __/ ___) |
|_| |_||_| |_| |____/
EOF
echo -e "🚀 Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}\n"
log "🚀 Starting HOPS Deployment v${SCRIPT_VERSION}"
# --------------------------------------------
# SYSTEM REQUIREMENTS CHECK
# --------------------------------------------
check_system_requirements() {
local MIN_RAM_GB=2
local MIN_DISK_GB=10
local MIN_CORES=2
log "🔍 Checking system requirements..."
# Check RAM
local RAM_GB=$(free -g | awk '/^Mem:/{print $2}')
if [[ $RAM_GB -lt $MIN_RAM_GB ]]; then
error_exit "Insufficient RAM: ${RAM_GB}GB detected, ${MIN_RAM_GB}GB required"
fi
# Check disk space
local DISK_AVAIL=$(df -BG --output=avail / | tail -n 1 | tr -d 'G')
if [[ $DISK_AVAIL -lt $MIN_DISK_GB ]]; then
error_exit "Insufficient disk space: ${DISK_AVAIL}GB available, ${MIN_DISK_GB}GB required"
fi
# Check CPU cores
local CPU_CORES=$(nproc)
if [[ $CPU_CORES -lt $MIN_CORES ]]; then
log "⚠️ Low CPU cores: ${CPU_CORES} detected, ${MIN_CORES} recommended"
fi
log "✅ System meets minimum requirements (${RAM_GB}GB RAM, ${CPU_CORES} cores, ${DISK_AVAIL}GB disk)"
}
# --------------------------------------------
# REQUIRED PACKAGES CHECK
# --------------------------------------------
check_required_packages() {
local missing_packages=()
local required_packages=("curl" "wget" "openssl" "lsof" "apache2-utils")
log "📦 Checking required packages..."
for package in "${required_packages[@]}"; do
if ! command -v "${package%%-*}" &>/dev/null; then
missing_packages+=("$package")
fi
done
if [[ ${#missing_packages[@]} -gt 0 ]]; then
log "📦 Installing missing packages: ${missing_packages[*]}"
apt-get update && apt-get install -y "${missing_packages[@]}"
fi
log "✅ All required packages are installed"
}
# --------------------------------------------
# ROOT CHECK
# --------------------------------------------
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root or with sudo."
fi
# --------------------------------------------
# OS DETECTION
# --------------------------------------------
detect_os() {
if command -v lsb_release &>/dev/null; then
OS_NAME=$(lsb_release -is)
OS_VERSION=$(lsb_release -rs)
else
OS_NAME=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"')
fi
OS_NAME_LOWER=$(echo "$OS_NAME" | tr '[:upper:]' '[:lower:]')
if [[ ! "$OS_NAME_LOWER" =~ ^(ubuntu|debian|linuxmint|mint)$ ]]; then
error_exit "Unsupported OS: $OS_NAME Only Debian/Ubuntu/Mint supported"
fi
log "✅ Detected OS: $OS_NAME $OS_VERSION"
}
# --------------------------------------------
# USER CONFIGURATION COLLECTION
# --------------------------------------------
collect_user_configuration() {
log "🔧 Collecting user configuration..."
# Get running user info
if [[ -n "$SUDO_USER" ]]; then
RUNNING_USER="$SUDO_USER"
PUID=$(id -u "$SUDO_USER")
PGID=$(id -g "$SUDO_USER")
else
RUNNING_USER="root"
PUID=1000
PGID=1000
log "⚠️ Running as root, defaulting to PUID=1000, PGID=1000"
fi
# Timezone configuration
echo -e "\n🌍 Timezone Configuration"
echo "Current timezone: $(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown")"
echo -e "Keep current timezone? [Y/n]: "
read -r keep_tz
if [[ "$keep_tz" =~ ^[Nn]$ ]]; then
echo -e "Enter timezone (e.g., America/New_York, Europe/London): "
read -r user_timezone
validate_timezone "$user_timezone"
TIMEZONE="$user_timezone"
else
TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "America/New_York")
fi
# Directory configuration
echo -e "\n📁 Directory Configuration"
echo -e "Media directory [/mnt/media]: "
read -r media_dir
MEDIA_DIR="${media_dir:-/mnt/media}"
echo -e "Application data directory [/opt/appdata]: "
read -r appdata_dir
APPDATA_DIR="${appdata_dir:-/opt/appdata}"
# Create directories
mkdir -p "$MEDIA_DIR"/{movies,tv,music,books,downloads}
mkdir -p "$APPDATA_DIR"
# Set ownership if not root
if [[ "$RUNNING_USER" != "root" ]]; then
chown -R "$PUID:$PGID" "$MEDIA_DIR" "$APPDATA_DIR" 2>/dev/null || true
fi
log "✅ User configuration collected"
log " User: $RUNNING_USER ($PUID:$PGID)"
log " Timezone: $TIMEZONE"
log " Media: $MEDIA_DIR"
log " AppData: $APPDATA_DIR"
}
# --------------------------------------------
# VALIDATION FUNCTIONS
# --------------------------------------------
validate_timezone() {
if ! timedatectl list-timezones | grep -qx "$1" 2>/dev/null; then
log "⚠️ Timezone '$1' invalid, defaulting to 'America/New_York'"
TIMEZONE="America/New_York"
fi
}
validate_password() {
local password="$1"
local min_length="${2:-12}"
if [[ -z "$password" ]]; then
echo -e "\n🔐 Password must meet these requirements:"
echo " • Minimum $min_length characters"
echo " • At least one uppercase letter"
echo " • At least one lowercase letter"
echo " • At least one number"
echo " • At least one special character"
return 3
fi
if [[ ${#password} -lt $min_length ]]; then
return 1
fi
if [[ ! "$password" =~ [A-Z] ]] || [[ ! "$password" =~ [a-z] ]] || \
[[ ! "$password" =~ [0-9] ]] || [[ ! "$password" =~ [^A-Za-z0-9] ]]; then
return 2
fi
return 0
}
check_port() {
local PORT=$1
local SERVICE=$2
if lsof -i :"$PORT" >/dev/null 2>&1; then
local PROCESS=$(lsof -ti :"$PORT" | head -1)
local PROCESS_NAME=$(ps -p "$PROCESS" -o comm= 2>/dev/null || echo "unknown")
log "⚠️ Port $PORT is already in use by $PROCESS_NAME. $SERVICE may fail to start."
return 1
fi
return 0
}
check_all_ports() {
local SERVICES=("$@")
local CONFLICTS=()
# Source service definitions to get port mappings
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
fi
for svc in "${SERVICES[@]}"; do
local ports=$(get_service_ports "$svc")
for port in $ports; do
if ! check_port "$port" "$svc"; then
CONFLICTS+=("Port $port ($svc)")
fi
done
done
if [[ ${#CONFLICTS[@]} -gt 0 ]]; then
log "⚠️ Found ${#CONFLICTS[@]} port conflicts:"
for conflict in "${CONFLICTS[@]}"; do
log "$conflict"
done
echo -e "\n⚠️ Port conflicts detected! Continue anyway? (y/N): "
read -r continue_choice
if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then
error_exit "Installation cancelled due to port conflicts."
fi
return 1
fi
return 0
}
# --------------------------------------------
# DOCKER COMPOSE VERSION CHECK
# --------------------------------------------
check_docker_compose_version() {
# Check for Docker Compose plugin (v2)
if docker compose version &>/dev/null; then
log "✅ Docker Compose plugin detected ($(docker compose version --short))"
return 0
fi
# Check for standalone docker-compose (v1)
if command -v docker-compose &>/dev/null; then
log "⚠️ Found legacy docker-compose (v1). Installing Docker Compose plugin..."
if ! apt-get install -y docker-compose-plugin 2>&1 | tee -a "$LOG_FILE"; then
error_exit "Failed to install Docker Compose plugin."
fi
return 0
fi
# Neither found
error_exit "No Docker Compose detected. Please install Docker first."
}
# --------------------------------------------
# IMPROVED PASSWORD GENERATION
# --------------------------------------------
generate_secure_password() {
local length="${1:-16}"
local max_attempts=5
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
# Generate password with mixed case, numbers, and symbols
local password=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-${length})
# Ensure it meets complexity requirements
if validate_password "$password" "$length"; then
echo "$password"
return 0
fi
((attempt++))
done
# Fallback: construct a guaranteed compliant password
local upper=$(tr -dc 'A-Z' < /dev/urandom | head -c2)
local lower=$(tr -dc 'a-z' < /dev/urandom | head -c4)
local digits=$(tr -dc '0-9' < /dev/urandom | head -c2)
local symbols=$(tr -dc '!@#$%^&*' < /dev/urandom | head -c2)
local remaining_length=$((length - 10))
if [[ $remaining_length -gt 0 ]]; then
local remaining=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c$remaining_length)
echo "${upper}${lower}${digits}${symbols}${remaining}" | fold -w1 | shuf | tr -d '\n'
else
echo "${upper}${lower}${digits}${symbols}"
fi
}
# --------------------------------------------
# ENVIRONMENT FILE GENERATION
# --------------------------------------------
create_env_file() {
local homelab_dir="$1"
log "📝 Creating environment file..."
cat > "$homelab_dir/.env" <<EOF
# HOPS Environment Configuration
# Generated on $(date)
# ==============================================
# CORE CONFIGURATION
# ==============================================
# User Configuration
PUID=$PUID
PGID=$PGID
TZ=$TIMEZONE
# Directory Configuration
DATA_ROOT=$MEDIA_DIR
CONFIG_ROOT=$APPDATA_DIR
HOMELAB_DIR=$homelab_dir
# Network Configuration
DOCKER_SUBNET=172.20.0.0/16
# ==============================================
# SECURITY & AUTHENTICATION
# ==============================================
# Default Passwords (CHANGE THESE IMMEDIATELY!)
DEFAULT_ADMIN_PASSWORD=$(generate_secure_password 16)
DEFAULT_DB_PASSWORD=$(generate_secure_password 20)
# Optional: Custom domain for reverse proxy
# DOMAIN=yourdomain.com
# Optional: Email for Let's Encrypt
# ACME_EMAIL=admin@yourdomain.com
# ==============================================
# SERVICE-SPECIFIC CONFIGURATION
# ==============================================
# Plex Configuration (Get token from: https://www.plex.tv/claim/)
PLEX_CLAIM_TOKEN=
# Watchtower Email Notifications (Optional)
WATCHTOWER_EMAIL_FROM=
WATCHTOWER_EMAIL_TO=
WATCHTOWER_EMAIL_SERVER=
WATCHTOWER_EMAIL_PORT=587
WATCHTOWER_EMAIL_USER=
WATCHTOWER_EMAIL_PASSWORD=
# Traefik Let's Encrypt Email
ACME_EMAIL=admin@localhost
EOF
chmod 600 "$homelab_dir/.env"
log "✅ Environment file created with secure permissions"
}
# --------------------------------------------
# SERVICE SELECTION
# --------------------------------------------
select_services() {
echo -e "\n📺 CORE MEDIA TOOLS"
echo "1) Sonarr 2) Radarr 3) Lidarr 4) Readarr"
echo "5) Bazarr 6) Prowlarr 7) Tdarr"
echo -e "\n⬇️ DOWNLOAD CLIENTS"
echo "8) NZBGet 9) SABnzbd 10) Transmission 11) qBittorrent"
echo -e "\n🎞️ MEDIA SERVERS"
echo "12) Plex 13) Jellyfin 14) Jellystat 15) Emby"
echo -e "\n🎛️ REQUEST MANAGEMENT"
echo "16) Overseerr 17) Jellyseerr 18) Ombi"
echo -e "\n🔒 NETWORK & SECURITY"
echo "19) Traefik 20) Nginx Proxy Manager 21) Authelia"
echo -e "\n📈 MONITORING"
echo "22) Portainer 23) Watchtower 24) Uptime Kuma"
echo -e "\n📝 Select services (space-separated numbers, or 'all' for everything): "
read -a service_choices
declare -A SERVICE_MAP=(
[1]="sonarr" [2]="radarr" [3]="lidarr" [4]="readarr"
[5]="bazarr" [6]="prowlarr" [7]="tdarr" [8]="nzbget"
[9]="sabnzbd" [10]="transmission" [11]="qbittorrent"
[12]="plex" [13]="jellyfin" [14]="jellystat" [15]="emby"
[16]="overseerr" [17]="jellyseerr" [18]="ombi"
[19]="traefik" [20]="nginx-proxy-manager" [21]="authelia"
[22]="portainer" [23]="watchtower" [24]="uptime-kuma"
)
SERVICES=()
if [[ "${service_choices[0]}" == "all" ]]; then
SERVICES=($(printf '%s\n' "${SERVICE_MAP[@]}" | sort))
log "🎯 Selected all services"
else
for choice in "${service_choices[@]}"; do
[[ -n "${SERVICE_MAP[$choice]}" ]] && SERVICES+=("${SERVICE_MAP[$choice]}")
done
fi
if [[ ${#SERVICES[@]} -eq 0 ]]; then
error_exit "No valid services selected."
fi
log "✅ Selected services: ${SERVICES[*]}"
# Check for service dependencies and conflicts
check_service_dependencies
check_service_conflicts
}
# --------------------------------------------
# DEPENDENCY AND CONFLICT CHECKING
# --------------------------------------------
check_service_dependencies() {
# Source service definitions for dependency resolution
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
# Resolve dependencies
local all_services=($(resolve_dependencies "${SERVICES[@]}"))
local deps_added=()
for service in "${all_services[@]}"; do
if [[ ! " ${SERVICES[*]} " =~ " ${service} " ]]; then
deps_added+=("$service")
fi
done
if [[ ${#deps_added[@]} -gt 0 ]]; then
log "📦 Adding dependencies: ${deps_added[*]}"
SERVICES=("${all_services[@]}")
fi
fi
# Check if any *arr services are selected without Prowlarr
local arr_services=(sonarr radarr lidarr readarr)
local has_arr=false
for arr in "${arr_services[@]}"; do
if [[ "${SERVICES[*]}" =~ $arr ]]; then
has_arr=true
break
fi
done
if [[ $has_arr == true ]] && [[ ! "${SERVICES[*]}" =~ prowlarr ]]; then
echo -e "\n💡 Recommendation: You selected *arr services but not Prowlarr."
echo "Prowlarr manages indexers for all *arr applications."
echo -e "Add Prowlarr? [Y/n]: "
read -r add_prowlarr
if [[ ! "$add_prowlarr" =~ ^[Nn]$ ]]; then
SERVICES+=("prowlarr")
log "✅ Added Prowlarr"
fi
fi
}
check_service_conflicts() {
local warnings=()
# Check for multiple media servers
local media_servers=(plex jellyfin emby)
local selected_media_servers=()
for server in "${media_servers[@]}"; do
if [[ "${SERVICES[*]}" =~ $server ]]; then
selected_media_servers+=("$server")
fi
done
if [[ ${#selected_media_servers[@]} -gt 1 ]]; then
warnings+=("Multiple media servers selected: ${selected_media_servers[*]}")
fi
# Check for multiple reverse proxies
local reverse_proxies=(traefik nginx-proxy-manager)
local selected_proxies=()
for proxy in "${reverse_proxies[@]}"; do
if [[ "${SERVICES[*]}" =~ $proxy ]]; then
selected_proxies+=("$proxy")
fi
done
if [[ ${#selected_proxies[@]} -gt 1 ]]; then
warnings+=("Multiple reverse proxies selected: ${selected_proxies[*]} (may conflict on ports 80/443)")
fi
# Display warnings if any
if [[ ${#warnings[@]} -gt 0 ]]; then
log "⚠️ Configuration warnings:"
for warning in "${warnings[@]}"; do
log "$warning"
done
echo -e "\n⚠️ Continue with this configuration? [y/N]: "
read -r continue_choice
if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then
log "🚫 Installation cancelled by user"
exit 0
fi
fi
}
# --------------------------------------------
# DOCKER COMPOSE FILE GENERATION
# --------------------------------------------
generate_docker_compose() {
local HOMELAB_DIR="$HOME/homelab"
mkdir -p "$HOMELAB_DIR"
cd "$HOMELAB_DIR"
if [[ -f docker-compose.yml ]]; then
local BACKUP_FILE="docker-compose.yml.bak.$(date +%Y%m%d%H%M%S)"
log "📝 Backing up existing compose file to $BACKUP_FILE"
mv docker-compose.yml "$BACKUP_FILE"
fi
log "📝 Generating Docker Compose configuration..."
create_env_file "$HOMELAB_DIR"
# Source the service definitions
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
# Export variables for service definitions
export PUID PGID TIMEZONE MEDIA_DIR APPDATA_DIR
# Generate complete compose file with all services
generate_complete_compose "${SERVICES[@]}"
track_step "compose_generated"
# Create service-specific configurations
create_service_configs "${SERVICES[@]}"
log "✅ Generated Docker Compose with ${#SERVICES[@]} services"
else
error_exit "Service definitions file not found: $SCRIPT_DIR/hops_service_definitions.sh"
fi
# Create networks if they don't exist
create_docker_networks
}
# --------------------------------------------
# NETWORK CREATION
# --------------------------------------------
create_docker_networks() {
log "🌐 Creating Docker networks..."
# Create traefik network if it doesn't exist
if ! docker network ls --format "{{.Name}}" | grep -q "^traefik$"; then
if docker network create traefik 2>/dev/null; then
log "✅ Created traefik network"
else
log "⚠️ Could not create traefik network (may already exist)"
fi
fi
}
# --------------------------------------------
# ENHANCED DEPLOYMENT WITH ROLLBACK
# --------------------------------------------
deploy_services() {
log "🚀 Starting deployment..."
# Set up error trap
trap 'error_exit_with_rollback "Deployment failed at step: ${BASH_COMMAND}"' ERR
# Pre-deployment checks
log "🔍 Running pre-deployment validation..."
if ! docker info >/dev/null 2>&1; then
error_exit_with_rollback "Docker daemon is not running or accessible"
fi
if ! docker compose config >/dev/null 2>&1; then
error_exit_with_rollback "Generated docker-compose.yml is invalid"
fi
# Create required directories
log "📁 Creating required directories..."
for svc in "${SERVICES[@]}"; do
mkdir -p "${APPDATA_DIR}/${svc}"
chown -R "$PUID:$PGID" "${APPDATA_DIR}/${svc}" 2>/dev/null || true
done
track_step "directories_created"
# Pull images with retry logic
log "📥 Pulling container images..."
local PULL_RETRIES=3
for attempt in $(seq 1 $PULL_RETRIES); do
if docker compose pull 2>&1 | tee -a "$LOG_FILE"; then
track_step "images_pulled"
break
elif [[ $attempt -eq $PULL_RETRIES ]]; then
error_exit_with_rollback "Failed to pull images after $PULL_RETRIES attempts"
else
log "⚠️ Pull attempt $attempt failed, retrying in 10 seconds..."
sleep 10
fi
done
# Start containers
log "🔄 Starting containers..."
if docker compose up -d 2>&1 | tee -a "$LOG_FILE"; then
track_step "containers_started"
else
log "❌ Some containers failed to start. Checking status..."
docker compose ps
error_exit_with_rollback "Container startup failed"
fi
# Clear trap on success
trap - ERR
}
# --------------------------------------------
# ENHANCED SERVICE VERIFICATION
# --------------------------------------------
verify_service_health() {
local service_name="$1"
local max_wait=300 # 5 minutes
local interval=10
log "🔍 Waiting for $service_name to be healthy..."
for ((i=0; i<max_wait; i+=interval)); do
local health=$(docker inspect --format='{{.State.Health.Status}}' "$service_name" 2>/dev/null || echo "none")
case "$health" in
"healthy")
log "$service_name is healthy"
return 0
;;
"starting")
log "$service_name is starting... (${i}s elapsed)"
;;
"unhealthy")
log "$service_name is unhealthy"
return 1
;;
"none")
# No health check defined, check if container is running
local state=$(docker inspect --format='{{.State.Status}}' "$service_name" 2>/dev/null || echo "unknown")
if [[ "$state" == "running" ]]; then
log "$service_name is running (no health check)"
return 0
fi
;;
esac
sleep "$interval"
done
log "⚠️ $service_name health check timed out"
return 1
}
verify_services() {
log "🩺 Verifying service health..."
local FAILED_SERVICES=()
for svc in "${SERVICES[@]}"; do
if docker ps --format "{{.Names}}" | grep -qi "^${svc}$"; then
if ! verify_service_health "$svc"; then
FAILED_SERVICES+=("$svc")
fi
else
log "$svc container not found"
FAILED_SERVICES+=("$svc")
fi
done
if [[ ${#FAILED_SERVICES[@]} -gt 0 ]]; then
log "⚠️ Services requiring attention:"
for svc in "${FAILED_SERVICES[@]}"; do
log "$svc - Check logs: docker logs $svc"
done
else
log "✅ All services are healthy"
fi
}
# --------------------------------------------
# SECURITY SETUP
# --------------------------------------------
setup_security() {
log "🔒 Applying security hardening..."
# Secure sensitive files
find "$APPDATA_DIR" -name "*.env" -exec chmod 600 {} \; 2>/dev/null || true
find "$APPDATA_DIR" -name "*.key" -exec chmod 600 {} \; 2>/dev/null || true
find "$APPDATA_DIR" -name "*.pem" -exec chmod 600 {} \; 2>/dev/null || true
# Set secure permissions on homelab directory
chmod 750 "$HOME/homelab"
log "✅ Security hardening applied"
}
setup_firewall() {
if command -v ufw &>/dev/null; then
log "🔥 Configuring UFW firewall..."
# Don't reset if already configured
if ! ufw status | grep -q "Status: active"; then
ufw --force reset >/dev/null 2>&1
ufw default deny incoming >/dev/null 2>&1
ufw default allow outgoing >/dev/null 2>&1
# Allow SSH
ufw allow ssh >/dev/null 2>&1
fi
# Allow service ports based on selection
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
for svc in "${SERVICES[@]}"; do
local ports=$(get_service_ports "$svc")
for port in $ports; do
# Skip UDP ports and handle TCP/UDP notation
if [[ "$port" =~ /udp$ ]]; then
local port_num="${port%/udp}"
ufw allow "$port_num/udp" comment "$svc" >/dev/null 2>&1
else
local port_num="${port%/tcp}"
ufw allow "$port_num/tcp" comment "$svc" >/dev/null 2>&1
fi
done
done
fi
ufw --force enable >/dev/null 2>&1
log "✅ Firewall configured"
else
log "️ UFW not available, skipping firewall configuration"
fi
}
# --------------------------------------------
# MAIN INSTALLATION FLOW
# --------------------------------------------
check_system_requirements
detect_os
check_required_packages
collect_user_configuration
select_services
check_all_ports "${SERVICES[@]}"
# Install dependencies
log "📦 Installing prerequisites..."
if ! apt-get update &>/dev/null; then
error_exit "Failed to update package lists. Check your internet connection."
fi
local REQUIRED_PACKAGES="ca-certificates curl gnupg lsb-release lsof ufw fail2ban openssl apache2-utils"
if ! apt-get install -y $REQUIRED_PACKAGES 2>&1 | tee -a "$LOG_FILE"; then
error_exit "Failed to install required packages."
fi
# Install Docker if not present
if ! command -v docker &>/dev/null; then
log "🐳 Installing Docker..."
if ! curl -fsSL https://get.docker.com | sh 2>&1 | tee -a "$LOG_FILE"; then
error_exit "Failed to install Docker."
fi
# Add user to docker group
if [[ -n "$SUDO_USER" ]]; then
usermod -aG docker "$SUDO_USER"
log "✅ Added $SUDO_USER to docker group (restart session to take effect)"
fi
else
log "✅ Docker already installed ($(docker --version))"
fi
check_docker_compose_version
# Ensure Docker daemon is running
if ! systemctl is-active --quiet docker; then
log "🔄 Starting Docker daemon..."
systemctl start docker || error_exit "Failed to start Docker daemon"
systemctl enable docker || log "⚠️ Could not enable Docker service"
fi
setup_firewall
generate_docker_compose
deploy_services
setup_security
verify_services
# --------------------------------------------
# FINAL SUMMARY
# --------------------------------------------
echo -e "\n🎉 HOPS Enhanced Deployment Complete!"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "📋 Deployment Summary:"
echo -e "\n📂 Configuration:"
echo " • Homelab Directory: $HOME/homelab"
echo " • Application Data: $APPDATA_DIR"
echo " • Media Directory: $MEDIA_DIR"
echo " • User/Group: $RUNNING_USER ($PUID:$PGID)"
echo " • Timezone: $TIMEZONE"
echo " • Log File: $LOG_FILE"
echo -e "\n🔐 Security:"
echo " • Generated secure passwords (see .env file)"
echo " • Firewall configured with service-specific rules"
echo " • File permissions hardened"
echo -e "\n📱 Deployed Services:"
local service_count=0
for svc in "${SERVICES[@]}"; do
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
local ports=$(get_service_ports "$svc")
local main_port=$(echo $ports | cut -d' ' -f1)
if [[ -n "$main_port" ]]; then
echo "$svc: http://$(hostname -I | awk '{print $1}'):$main_port"
((service_count++))
fi
fi
done
echo -e "\n🔧 Management Commands:"
echo " • View all logs: docker compose logs -f"
echo " • View service logs: docker compose logs -f [service]"
echo " • Restart service: docker compose restart [service]"
echo " • Stop services: docker compose down"
echo " • Start services: docker compose up -d"
echo " • Update services: docker compose pull && docker compose up -d"
echo -e "\n📚 Next Steps:"
echo " 1. Access services using the URLs above"
echo " 2. Change default passwords from .env file"
echo " 3. Configure services according to your needs"
echo " 4. Set up your media library paths"
echo -e "\n📋 Logs and troubleshooting: $LOG_FILE"
echo -e "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log "🎉 HOPS Enhanced deployment completed successfully!"
return 0
}
+1285
View File
File diff suppressed because it is too large Load Diff
+560
View File
@@ -0,0 +1,560 @@
#!/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="3.1.0"
# --------------------------------------------
# LOGGING SETUP
# --------------------------------------------
local LOG_DIR="/var/log/hops"
local LOG_FILE="$LOG_DIR/homelab-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/homelab"
"/home/*/homelab"
"/opt/homelab"
"/srv/homelab"
)
# Try to find from running user's home first
if [[ -n "$SUDO_USER" ]]; then
local user_home=$(eval echo "~$SUDO_USER")
POSSIBLE_DIRS=("$user_home/homelab" "${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
}