Rename scripts for clarity and add Huntarr service

- Renamed all scripts to descriptive names without prefixes:
  • hops.sh → hops (main script)
  • hops_installer_enhanced.sh → install
  • hops_uninstaller_fixed.sh → uninstall
  • hops_service_definitions.sh → services
  • hops_install.sh → setup
  • hops_privileged_setup.sh → privileged-setup
  • hops_user_operations.sh → user-operations
  • hops_service_definitions_improved.sh → services-improved

- Added Huntarr service support:
  • Docker image: ghcr.io/plexguide/huntarr:latest
  • Port: 9705 with /health endpoint
  • Missing media discovery and automation
  • Integrates with *arr stack services
  • Added to installer menu as option 8

- Updated all script references and documentation
- Updated service categories in README and CLAUDE.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Stephen Klein
2025-07-17 21:52:22 -04:00
parent 721f7d7a75
commit 5affcd2e26
17 changed files with 1077 additions and 3678 deletions
+114 -23
View File
@@ -6,54 +6,85 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
HOPS (Homelab Orchestration Provisioning Script) is a comprehensive automation tool for deploying homelab infrastructure using Docker Compose. It provides menu-driven installation, management, and monitoring of popular homelab services including media servers, download clients, monitoring tools, and more.
**Cross-Platform Support**: HOPS now supports both Linux (Ubuntu/Debian/Mint) and macOS systems with intelligent platform detection and abstraction.
## Architecture
### Core Components
- **Main Script (`hops.sh`)**: Primary entry point providing menu-driven interface for all operations
- **Installer (`hops_installer_enhanced.sh`)**: Handles service installation and Docker Compose deployment
- **Uninstaller (`hops_uninstaller_fixed.sh`)**: Manages complete removal of services and configurations
- **Service Definitions (`hops_service_definitions.sh`)**: Contains Docker Compose service templates and configurations
- **Installer (`install`)**: Handles service installation and Docker Compose deployment
- **Uninstaller (`uninstall`)**: Manages complete removal of services and configurations
- **Service Definitions (`services`)**: Contains Docker Compose service templates and configurations
- **Library System (`lib/`)**: Modular abstraction layer for cross-platform compatibility
- `lib/common.sh`: Shared logging, UI, and utility functions
- `lib/system.sh`: OS detection, system requirements, and platform abstraction
### Key Design Patterns
- **Modular Architecture**: Each major function is separated into dedicated scripts
- **Cross-Platform Abstraction**: OS-specific operations are abstracted through lib/system.sh
- **Service-Driven**: All services are defined as Docker Compose configurations with standardized patterns
- **Error Handling**: Comprehensive error handling with logging and rollback capabilities
- **Security First**: Built-in security hardening, firewall configuration, and secure password generation
- **Security First**: Built-in security hardening, platform-appropriate firewall configuration, and secure password generation
## Development Commands
### Running HOPS
```bash
# Main script (requires root)
# Main script (requires root on Linux, admin on macOS)
sudo ./hops.sh
# Direct installation
sudo ./hops_installer_enhanced.sh
# Direct installation (Linux)
sudo ./install
# Direct installation (macOS)
sudo ./install
# Uninstallation
sudo ./hops_uninstaller_fixed.sh
sudo ./uninstall
```
### Platform-Specific Requirements
**Linux (Ubuntu/Debian/Mint):**
- Root/sudo access
- Internet connection
- 2GB+ RAM, 10GB+ disk space
**macOS:**
- Admin access
- Internet connection
- 2GB+ RAM, 10GB+ disk space
- Homebrew will be installed automatically if not present
- Docker Desktop will be installed automatically via Homebrew
### Testing and Validation
```bash
# Check script syntax
bash -n hops.sh
bash -n hops_installer_enhanced.sh
bash -n hops_service_definitions.sh
bash -n hops_uninstaller_fixed.sh
bash -n install
bash -n services
bash -n uninstall
bash -n lib/system.sh
bash -n lib/common.sh
# Test OS detection and system requirements
source lib/common.sh && source lib/system.sh && detect_os && check_system_requirements 2 10
# Test service definitions
source hops_service_definitions.sh
source services
generate_service_definition jellyfin
```
### Log Management
```bash
# View installation logs
# View installation logs (Linux)
sudo tail -f /var/log/hops/hops-main-*.log
# View installation logs (macOS)
sudo tail -f /usr/local/var/log/hops/hops-main-*.log
# View Docker Compose logs
cd ~/homelab && docker compose logs -f [service-name]
```
@@ -63,13 +94,16 @@ cd ~/homelab && docker compose logs -f [service-name]
### Service Definition Pattern
All services follow a standardized Docker Compose pattern:
- LinuxServer.io containers with PUID/PGID/TZ environment variables
- Consistent volume mounting (`/opt/appdata` for configs, `/mnt/media` for data)
- Platform-aware volume mounting:
- **Linux**: `/opt/appdata` for configs, `/mnt/media` for data
- **macOS**: `/Users/[user]/homelab/config` for configs, `/Users/[user]/homelab/media` for data
- Health checks for web services
- Unified network configuration (`homelab` network)
- Restart policy: `unless-stopped`
- Platform-specific features (timezone mounts, GPU access) handled automatically
### Supported Service Categories
1. **Media Management**: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr, Tdarr
1. **Media Management**: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr, Tdarr, Huntarr
2. **Download Clients**: qBittorrent, Transmission, NZBGet, SABnzbd
3. **Media Servers**: Jellyfin, Plex, Emby, Jellystat
4. **Request Management**: Overseerr, Jellyseerr, Ombi
@@ -78,6 +112,7 @@ All services follow a standardized Docker Compose pattern:
## File Structure
### Linux File Structure
```
~/homelab/ # Main deployment directory
├── docker-compose.yml # Generated service definitions
@@ -94,6 +129,21 @@ All services follow a standardized Docker Compose pattern:
└── downloads/
```
### macOS File Structure
```
~/homelab/ # Main deployment directory
├── docker-compose.yml # Generated service definitions
├── .env # Environment variables
├── logs/ # Application logs
├── config/ # Application configurations
│ └── [service-name]/ # Individual service configs
└── media/ # Media storage
├── movies/
├── tv/
├── music/
└── downloads/
```
## Environment Configuration
Key environment variables in `~/homelab/.env`:
@@ -105,7 +155,9 @@ Key environment variables in `~/homelab/.env`:
## Security Features
- **Firewall Integration**: Automatic UFW rule management
- **Firewall Integration**:
- **Linux**: Automatic UFW rule management
- **macOS**: Manual firewall configuration (automatic setup skipped)
- **Secure Password Generation**: Cryptographically secure passwords
- **File Permission Hardening**: Restrictive permissions on sensitive files
- **Network Isolation**: Docker network segregation
@@ -113,7 +165,9 @@ Key environment variables in `~/homelab/.env`:
## Error Handling
- **Comprehensive Logging**: All operations logged to `/var/log/hops/`
- **Comprehensive Logging**: All operations logged to platform-specific directories
- **Linux**: `/var/log/hops/`
- **macOS**: `/usr/local/var/log/hops/`
- **Rollback Capability**: Automatic rollback on deployment failure
- **Dependency Validation**: Pre-deployment system requirement checks
- **Service Health Monitoring**: Built-in health checks for all services
@@ -126,35 +180,72 @@ Key environment variables in `~/homelab/.env`:
- `show_service_status()`: Real-time monitoring
- `show_access_info()`: Service URL and credential display
### In `hops_service_definitions.sh`
### In `services`
- `generate_service_definition()`: Creates Docker Compose service blocks
- `get_linuxserver_env()`: Standard environment variables
- `get_web_healthcheck()`: Health check configurations
- `get_timezone_mount()`: Platform-specific timezone handling
- `get_gpu_devices()`: Platform-specific GPU access
### In `hops_installer_enhanced.sh`
### In `install`
- Service selection and dependency resolution
- Docker Compose file generation
- Cross-platform dependency installation
- Security hardening implementation
- Post-deployment verification
### In `lib/system.sh`
- `detect_os()`: Cross-platform OS detection
- `check_system_requirements()`: Platform-aware system validation
- `install_package()`: Package manager abstraction
- `install_docker()`: Platform-specific Docker installation
- `get_primary_ip()`: Network interface detection
- `get_default_*_path()`: Platform-specific path resolution
## Development Guidelines
- **Bash Best Practices**: Use `set -e` for error handling, quote variables, use readonly for constants
- **Cross-Platform Compatibility**: Always use lib/system.sh abstraction functions instead of direct OS commands
- **Logging**: Use the logging functions (`log`, `error_exit`, `warning`, `success`, `info`)
- **Color Output**: Use predefined color constants for consistent formatting
- **Service Patterns**: Follow the established Docker Compose patterns when adding new services
- **Security**: Never commit secrets, use secure password generation, implement proper file permissions
- **Path Handling**: Use `get_default_*_path()` functions for platform-specific paths
## Common Operations
### Adding New Services
1. Add service definition function in `hops_service_definitions.sh`
2. Add service to installer menu in `hops_installer_enhanced.sh`
1. Add service definition function in `services`
2. Add service to installer menu in `install`
3. Configure any required dependencies or special handling
4. Test deployment and health checks
### Debugging Issues
1. Check logs in `/var/log/hops/`
1. Check logs in platform-specific directories:
- **Linux**: `/var/log/hops/`
- **macOS**: `/usr/local/var/log/hops/`
2. Verify Docker Compose syntax with `docker compose config`
3. Check service health with `docker compose ps`
4. Review firewall rules with `sudo ufw status`
4. Review firewall rules:
- **Linux**: `sudo ufw status`
- **macOS**: Check System Preferences > Security & Privacy > Firewall
## Platform-Specific Notes
### macOS Considerations
- **Architecture Support**: Both Intel (x86_64) and Apple Silicon (ARM64) are supported
- **Docker Desktop**: Automatically installed via Homebrew if not present
- **Homebrew**: Automatically installed if not present
- **GPU Acceleration**: Not available (Docker containers cannot access macOS GPU)
- **Firewall**: Manual configuration required (automatic UFW setup skipped)
- **File Permissions**: Uses user's home directory structure for better compatibility
- **Service Management**: Uses launchctl instead of systemctl where applicable
### Linux Considerations
- **Architecture Support**: x86_64 only
- **Docker Engine**: Installed via official Docker script
- **Package Management**: Uses apt-get for Ubuntu/Debian/Mint
- **GPU Acceleration**: Available for Intel GPUs via /dev/dri passthrough
- **Firewall**: Automatic UFW configuration
- **File Permissions**: Uses system-wide directories (/opt, /mnt)
- **Service Management**: Uses systemctl for service management
+17 -16
View File
@@ -21,7 +21,7 @@
- **📖 Documentation**: Complete `CLAUDE.md` for development guidance
### Installation Methods
- **🚀 New Secure Installer**: `sudo ./hops_install.sh` - Recommended method
- **🚀 New Secure Installer**: `sudo ./setup` - Recommended method
- **⚙️ Manual Installation**: Separate privileged and user operations
- **🔄 Legacy Support**: Original `hops.sh` still fully supported
@@ -78,6 +78,7 @@ HOPS (Homelab Orchestration Provisioning Script) automates the deployment of a c
- **Bazarr** - Subtitle management
- **Prowlarr** - Indexer management
- **Tdarr** - Media transcoding
- **Huntarr** - Missing media discovery and automation
### ⬇️ Download Clients
- **qBittorrent** - Feature-rich BitTorrent client
@@ -131,12 +132,12 @@ chmod +x *.sh
### 2. Run Installation (New Improved Method)
```bash
# Option 1: Use the new secure installation wrapper
sudo ./hops_install.sh
sudo ./setup
# Option 2: Manual two-phase installation
sudo ./hops_privileged_setup.sh # Run as root
./hops_user_operations.sh generate <services> # Run as user
./hops_user_operations.sh deploy # Run as user
sudo ./privileged-setup # Run as root
./user-operations generate <services> # Run as user
./user-operations deploy # Run as user
# Option 3: Legacy installation (still supported)
sudo ./hops.sh
@@ -230,10 +231,10 @@ ACME_EMAIL=admin@yourdomain.com
### Service Management Commands
```bash
# NEW: User operations script (runs without sudo)
./hops_user_operations.sh status # View service status
./hops_user_operations.sh logs <service> # View service logs
./hops_user_operations.sh deploy # Deploy services
./hops_user_operations.sh stop # Stop all services
./user-operations status # View service status
./user-operations logs <service> # View service logs
./user-operations deploy # Deploy services
./user-operations stop # Stop all services
# Legacy: Direct Docker Compose commands
cd ~/homelab
@@ -257,10 +258,10 @@ hops/
│ ├── validation.sh # Input validation
│ ├── secrets.sh # Secret management
│ └── privileges.sh # Privilege management
├── hops_install.sh # NEW: Installation wrapper
├── hops_privileged_setup.sh # NEW: Root-only operations
├── hops_user_operations.sh # NEW: User operations
├── hops_service_definitions_improved.sh # NEW: Enhanced service definitions
├── setup # NEW: Installation wrapper
├── privileged-setup # NEW: Root-only operations
├── user-operations # NEW: User operations
├── services-improved # NEW: Enhanced service definitions
└── hops.sh # Legacy main script (still supported)
```
@@ -378,11 +379,11 @@ bash -n lib/*.sh
bash -n *.sh
# Test service definitions
./hops_service_definitions_improved.sh list
./hops_service_definitions_improved.sh generate jellyfin
./services-improved list
./services-improved generate jellyfin
# Test new installation method
sudo ./hops_install.sh
sudo ./setup
# Test legacy method
sudo ./hops.sh
-925
View File
@@ -1,925 +0,0 @@
#!/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
}
File diff suppressed because it is too large Load Diff
+23 -17
View File
@@ -13,9 +13,9 @@ 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"
readonly INSTALLER_SCRIPT="$SCRIPT_DIR/install"
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/uninstall"
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/services"
# Color codes for output
readonly RED='\033[0;31m'
@@ -27,16 +27,16 @@ 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"
# Load system utilities
source "$SCRIPT_DIR/lib/system.sh"
# Logging setup (will be set by setup_logging)
LOG_DIR=""
LOG_FILE=""
# Initialize logging
init_logging() {
if [[ $EUID -eq 0 ]]; then
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
fi
setup_logging "hops-main"
}
# Logging function
@@ -130,7 +130,9 @@ check_system_requirements() {
info "Checking system requirements..."
# Check OS
if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then
# OS check is handled by lib/system.sh
detect_os
if [[ "$OS_NAME_LOWER" != "macos" && ! "$OS_NAME_LOWER" =~ ^(ubuntu|debian|mint)$ ]]; then
warning "This script is designed for Ubuntu/Debian/Mint systems"
echo -e "Continue anyway? [y/N]: "
read -r continue_choice
@@ -442,7 +444,7 @@ show_access_info() {
echo -e "${BLUE}📱 Access your services at:${NC}"
# Get local IP
local local_ip=$(hostname -I | awk '{print $1}')
local local_ip=$(get_primary_ip)
# Service URLs with paths
local services=(
@@ -578,14 +580,18 @@ show_help() {
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"
local default_homelab_path=$(get_default_homelab_path)
local default_config_path=$(get_default_config_path)
local default_media_path=$(get_default_media_path)
echo -e " • Homelab directory: $default_homelab_path/"
echo -e " • App configurations: $default_config_path/"
echo -e " • Media storage: $default_media_path/"
echo -e " • Logs: $LOG_DIR/\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 " • Verify Docker is running: docker info"
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"
-679
View File
@@ -1,679 +0,0 @@
#!/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
-560
View File
@@ -1,560 +0,0 @@
#!/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
}
+124 -113
View File
@@ -11,22 +11,17 @@ install_hops() {
local SCRIPT_VERSION="3.1.0"
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Load system utilities
source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/system.sh"
# --------------------------------------------
# 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"
setup_logging "homelab-setup"
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
local_error_exit() {
error_exit "$1"
}
# Enhanced error handling with rollback
@@ -89,32 +84,20 @@ EOF
# --------------------------------------------
# SYSTEM REQUIREMENTS CHECK
# --------------------------------------------
check_system_requirements() {
validate_system_requirements() {
local MIN_RAM_GB=2
local MIN_DISK_GB=10
local MIN_CORES=2
log "🔍 Checking system requirements..."
info "🔍 Validating 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
# Detect OS first
detect_os
# 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 system requirements using new abstraction
check_system_requirements $MIN_RAM_GB $MIN_DISK_GB
# 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)"
return 0
}
# --------------------------------------------
@@ -122,22 +105,37 @@ EOF
# --------------------------------------------
check_required_packages() {
local missing_packages=()
local required_packages=("curl" "wget" "openssl" "lsof" "apache2-utils")
local required_packages=("curl" "wget" "openssl" "lsof")
log "📦 Checking required packages..."
# Add OS-specific packages
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
required_packages+=("httpd") # Apache on macOS (for htpasswd)
else
required_packages+=("apache2-utils") # Apache utils on Linux
fi
info "📦 Checking required packages..."
for package in "${required_packages[@]}"; do
if ! command -v "${package%%-*}" &>/dev/null; then
local check_cmd="${package%%-*}"
if [[ "$package" == "httpd" ]]; then
check_cmd="htpasswd" # Check for htpasswd command on macOS
fi
if ! command -v "$check_cmd" &>/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[@]}"
info "📦 Installing missing packages: ${missing_packages[*]}"
for package in "${missing_packages[@]}"; do
install_package "$package"
done
fi
log "✅ All required packages are installed"
success "✅ All required packages are installed"
}
# --------------------------------------------
@@ -147,24 +145,7 @@ EOF
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"
}
# OS detection is handled by the lib/system.sh functions
# --------------------------------------------
# USER CONFIGURATION COLLECTION
@@ -201,13 +182,16 @@ EOF
# Directory configuration
echo -e "\n📁 Directory Configuration"
echo -e "Media directory [/mnt/media]: "
read -r media_dir
MEDIA_DIR="${media_dir:-/mnt/media}"
local default_media_path=$(get_default_media_path)
local default_config_path=$(get_default_config_path)
echo -e "Application data directory [/opt/appdata]: "
echo -e "Media directory [$default_media_path]: "
read -r media_dir
MEDIA_DIR="${media_dir:-$default_media_path}"
echo -e "Application data directory [$default_config_path]: "
read -r appdata_dir
APPDATA_DIR="${appdata_dir:-/opt/appdata}"
APPDATA_DIR="${appdata_dir:-$default_config_path}"
# Create directories
mkdir -p "$MEDIA_DIR"/{movies,tv,music,books,downloads}
@@ -278,8 +262,8 @@ EOF
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"
if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/services"
fi
for svc in "${SERVICES[@]}"; do
@@ -438,43 +422,66 @@ EOF
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 "5) Bazarr 6) Prowlarr 7) Tdarr 8) Huntarr"
echo -e "\n⬇️ DOWNLOAD CLIENTS"
echo "8) NZBGet 9) SABnzbd 10) Transmission 11) qBittorrent"
echo "9) NZBGet 10) SABnzbd 11) Transmission 12) qBittorrent"
echo -e "\n🎞️ MEDIA SERVERS"
echo "12) Plex 13) Jellyfin 14) Jellystat 15) Emby"
echo "13) Plex 14) Jellyfin 15) Jellystat 16) Emby"
echo -e "\n🎛️ REQUEST MANAGEMENT"
echo "16) Overseerr 17) Jellyseerr 18) Ombi"
echo "17) Overseerr 18) Jellyseerr 19) Ombi"
echo -e "\n🔒 NETWORK & SECURITY"
echo "19) Traefik 20) Nginx Proxy Manager 21) Authelia"
echo "20) Traefik 21) Nginx Proxy Manager 22) Authelia"
echo -e "\n📈 MONITORING"
echo "22) Portainer 23) Watchtower 24) Uptime Kuma"
echo "23) Portainer 24) Watchtower 25) 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"
)
# Function to map service numbers to names (bash 3.2 compatible)
get_service_name() {
case "$1" in
1) echo "sonarr" ;;
2) echo "radarr" ;;
3) echo "lidarr" ;;
4) echo "readarr" ;;
5) echo "bazarr" ;;
6) echo "prowlarr" ;;
7) echo "tdarr" ;;
8) echo "huntarr" ;;
9) echo "nzbget" ;;
10) echo "sabnzbd" ;;
11) echo "transmission" ;;
12) echo "qbittorrent" ;;
13) echo "plex" ;;
14) echo "jellyfin" ;;
15) echo "jellystat" ;;
16) echo "emby" ;;
17) echo "overseerr" ;;
18) echo "jellyseerr" ;;
19) echo "ombi" ;;
20) echo "traefik" ;;
21) echo "nginx-proxy-manager" ;;
22) echo "authelia" ;;
23) echo "portainer" ;;
24) echo "watchtower" ;;
25) echo "uptime-kuma" ;;
*) echo "" ;;
esac
}
SERVICES=()
if [[ "${service_choices[0]}" == "all" ]]; then
SERVICES=($(printf '%s\n' "${SERVICE_MAP[@]}" | sort))
SERVICES=("sonarr" "radarr" "lidarr" "readarr" "bazarr" "prowlarr" "tdarr" "huntarr" "nzbget" "sabnzbd" "transmission" "qbittorrent" "plex" "jellyfin" "jellystat" "emby" "overseerr" "jellyseerr" "ombi" "traefik" "nginx-proxy-manager" "authelia" "portainer" "watchtower" "uptime-kuma")
log "🎯 Selected all services"
else
for choice in "${service_choices[@]}"; do
[[ -n "${SERVICE_MAP[$choice]}" ]] && SERVICES+=("${SERVICE_MAP[$choice]}")
service_name=$(get_service_name "$choice")
[[ -n "$service_name" ]] && SERVICES+=("$service_name")
done
fi
@@ -494,8 +501,8 @@ EOF
# --------------------------------------------
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"
if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/services"
# Resolve dependencies
local all_services=($(resolve_dependencies "${SERVICES[@]}"))
@@ -598,8 +605,8 @@ EOF
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"
if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/services"
# Export variables for service definitions
export PUID PGID TIMEZONE MEDIA_DIR APPDATA_DIR
@@ -613,7 +620,7 @@ EOF
log "✅ Generated Docker Compose with ${#SERVICES[@]} services"
else
error_exit "Service definitions file not found: $SCRIPT_DIR/hops_service_definitions.sh"
error_exit "Service definitions file not found: $SCRIPT_DIR/services"
fi
# Create networks if they don't exist
@@ -777,6 +784,11 @@ EOF
}
setup_firewall() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
info "🔥 Skipping firewall configuration on macOS (configure manually if needed)"
return 0
fi
if command -v ufw &>/dev/null; then
log "🔥 Configuring UFW firewall..."
@@ -819,47 +831,43 @@ EOF
# --------------------------------------------
# MAIN INSTALLATION FLOW
# --------------------------------------------
check_system_requirements
detect_os
validate_system_requirements
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."
# Install dependencies using abstraction
info "📦 Installing prerequisites..."
# Define packages based on OS
local required_packages
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
required_packages=("curl" "openssl" "lsof" "httpd")
else
required_packages=("ca-certificates" "curl" "gnupg" "lsb-release" "lsof" "ufw" "fail2ban" "openssl" "apache2-utils")
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 each package
for package in "${required_packages[@]}"; do
if ! command -v "${package%%-*}" &>/dev/null; then
install_package "$package"
fi
done
# 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
if ! check_docker_installation; then
install_docker
else
log "✅ Docker already installed ($(docker --version))"
success "✅ Docker already installed and running"
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"
if ! is_service_running docker; then
start_service docker
enable_service docker
fi
setup_firewall
@@ -891,12 +899,12 @@ EOF
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"
if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/services"
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"
echo " • $svc: http://$(get_primary_ip):$main_port"
((service_count++))
fi
fi
@@ -922,3 +930,6 @@ EOF
log "🎉 HOPS Enhanced deployment completed successfully!"
return 0
}
# Execute the main installation function
install_hops
+22 -4
View File
@@ -4,6 +4,12 @@
# Shared functions for logging, error handling, and UI
# Version: 3.1.0
# Prevent multiple sourcing
if [[ -n "${HOPS_COMMON_LOADED:-}" ]]; then
return 0
fi
readonly HOPS_COMMON_LOADED=1
# Color codes for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
@@ -27,7 +33,13 @@ setup_logging() {
return 1
fi
LOG_DIR="/var/log/hops"
# Set platform-specific log directory
if [[ "$(uname -s)" == "Darwin" ]]; then
LOG_DIR="/usr/local/var/log/hops"
else
LOG_DIR="/var/log/hops"
fi
LOG_FILE="$LOG_DIR/${log_prefix}-$(date +%Y%m%d-%H%M%S).log"
if [[ $EUID -eq 0 ]]; then
@@ -214,10 +226,16 @@ is_valid_ip() {
local regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
if [[ $ip =~ $regex ]]; then
local IFS='.'
local -a octets=($ip)
# Split IP into octets using parameter expansion
local octet1="${ip%%.*}"
local temp="${ip#*.}"
local octet2="${temp%%.*}"
temp="${temp#*.}"
local octet3="${temp%%.*}"
local octet4="${temp#*.}"
for octet in "${octets[@]}"; do
# Check each octet
for octet in "$octet1" "$octet2" "$octet3" "$octet4"; do
if [[ $octet -gt 255 ]]; then
return 1
fi
+9 -9
View File
@@ -449,7 +449,7 @@ EOF
# Generate service definitions
for service in "${services[@]}"; do
if "$SCRIPT_DIR/hops_service_definitions_improved.sh" generate "$service" >> "$compose_file"; then
if "$SCRIPT_DIR/services-improved" generate "$service" >> "$compose_file"; then
success "Added service: $service"
else
error_exit "Failed to generate service definition for: $service"
@@ -616,7 +616,7 @@ fi
# Phase 1: Privileged setup
info "📋 Phase 1: Privileged setup (requires root)"
if "$SCRIPT_DIR/hops_privileged_setup.sh"; then
if "$SCRIPT_DIR/privileged-setup"; then
success "Privileged setup completed"
else
error_exit "Privileged setup failed"
@@ -651,7 +651,7 @@ case "$choice" in
;;
4)
echo "Available services:"
"$SCRIPT_DIR/hops_service_definitions_improved.sh" list
"$SCRIPT_DIR/services-improved" list
read -p "Enter service names (space-separated): " -a services
;;
*)
@@ -661,10 +661,10 @@ case "$choice" in
esac
# Generate and deploy
if "$SCRIPT_DIR/hops_user_operations.sh" generate "${services[@]}"; then
if "$SCRIPT_DIR/user-operations" generate "${services[@]}"; then
echo "Configuration generated successfully"
if "$SCRIPT_DIR/hops_user_operations.sh" deploy; then
if "$SCRIPT_DIR/user-operations" deploy; then
echo "Services deployed successfully"
else
echo "Deployment failed"
@@ -677,7 +677,7 @@ fi
USERSCRIPT
success "Installation completed successfully"
success "Services are now running. Check status with: ./hops_user_operations.sh status"
success "Services are now running. Check status with: ./user-operations status"
EOF
chmod +x "$wrapper_script"
@@ -703,9 +703,9 @@ main() {
;;
"create-all")
create_privileged_setup "hops_privileged_setup.sh"
create_user_script "hops_user_operations.sh"
create_installation_wrapper "hops_install.sh"
create_privileged_setup "privileged-setup"
create_user_script "user-operations"
create_installation_wrapper "setup"
;;
"run")
+686 -13
View File
@@ -5,8 +5,8 @@
# Version: 3.1.0
# Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$LIB_DIR/common.sh"
# Global variables for system info
OS_NAME=""
@@ -17,6 +17,16 @@ OS_NAME_LOWER=""
detect_os() {
info "🔍 Detecting operating system..."
# Check if we're on macOS
if [[ "$(uname -s)" == "Darwin" ]]; then
OS_NAME="macOS"
OS_VERSION=$(sw_vers -productVersion)
OS_NAME_LOWER="macos"
success "Detected supported OS: $OS_NAME $OS_VERSION"
return 0
fi
# Linux detection
if command_exists lsb_release; then
OS_NAME=$(lsb_release -is)
OS_VERSION=$(lsb_release -rs)
@@ -35,7 +45,7 @@ detect_os() {
success "Detected supported OS: $OS_NAME $OS_VERSION"
;;
*)
error_exit "Unsupported OS: $OS_NAME $OS_VERSION. Only Ubuntu/Debian/Linux Mint are supported."
error_exit "Unsupported OS: $OS_NAME $OS_VERSION. Only Ubuntu/Debian/Linux Mint/macOS are supported."
;;
esac
}
@@ -50,13 +60,24 @@ check_system_requirements() {
# Check architecture
local arch=$(uname -m)
if [[ "$arch" != "x86_64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 is supported."
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS supports both x86_64 and arm64 (Apple Silicon)
if [[ "$arch" != "x86_64" && "$arch" != "arm64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 and arm64 are supported on macOS."
fi
else
# Linux only supports x86_64
if [[ "$arch" != "x86_64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 is supported."
fi
fi
# Check RAM
local ram_gb
if command_exists free; then
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS memory check
ram_gb=$(sysctl -n hw.memsize | awk '{print int($1/1024/1024/1024)}')
elif command_exists free; then
ram_gb=$(free -g | awk '/^Mem:/{print $2}')
else
ram_gb=$(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo)
@@ -68,7 +89,10 @@ check_system_requirements() {
# Check disk space
local disk_avail_gb
if command_exists df; then
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS disk space check
disk_avail_gb=$(df -g "$target_dir" | tail -n 1 | awk '{print $4}')
elif command_exists df; then
disk_avail_gb=$(df -BG --output=avail "$target_dir" | tail -n 1 | tr -d 'G')
else
error_exit "Unable to check disk space - 'df' command not available"
@@ -79,7 +103,13 @@ check_system_requirements() {
fi
# Check CPU cores
local cpu_cores=$(nproc)
local cpu_cores
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
cpu_cores=$(sysctl -n hw.ncpu)
else
cpu_cores=$(nproc)
fi
if [[ $cpu_cores -lt 2 ]]; then
warning "Only ${cpu_cores} CPU core(s) detected. 2+ cores recommended for optimal performance."
fi
@@ -189,7 +219,12 @@ check_sudo() {
# Get system timezone
get_system_timezone() {
if [[ -f /etc/timezone ]]; then
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS timezone detection
readlink /etc/localtime | sed 's|/var/db/timezone/zoneinfo/||' 2>/dev/null || \
ls -la /etc/localtime | awk '{print $NF}' | sed 's|/var/db/timezone/zoneinfo/||' 2>/dev/null || \
echo "UTC"
elif [[ -f /etc/timezone ]]; then
cat /etc/timezone
elif [[ -L /etc/localtime ]]; then
readlink /etc/localtime | sed 's|/usr/share/zoneinfo/||'
@@ -206,8 +241,16 @@ validate_timezone() {
return 1
fi
if [[ -f "/usr/share/zoneinfo/$timezone" ]]; then
return 0
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS timezone validation
if [[ -f "/var/db/timezone/zoneinfo/$timezone" ]]; then
return 0
fi
else
# Linux timezone validation
if [[ -f "/usr/share/zoneinfo/$timezone" ]]; then
return 0
fi
fi
return 1
@@ -221,7 +264,12 @@ check_storage_space() {
# Create directory if it doesn't exist
mkdir -p "$path" 2>/dev/null || true
local available_gb=$(df -BG --output=avail "$path" | tail -n 1 | tr -d 'G')
local available_gb
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
available_gb=$(df -g "$path" | tail -n 1 | awk '{print $4}')
else
available_gb=$(df -BG --output=avail "$path" | tail -n 1 | tr -d 'G')
fi
if [[ $available_gb -lt $required_gb ]]; then
error_exit "Insufficient storage space in $path: ${available_gb}GB available, ${required_gb}GB required"
@@ -273,6 +321,13 @@ get_user_info() {
check_firewall() {
info "🔥 Checking firewall status..."
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS uses pfctl/firewall, but we'll skip automatic configuration
warning "macOS firewall detected. Automatic firewall configuration skipped."
info "💡 You may need to manually configure firewall rules if needed."
return 0
fi
if command_exists ufw; then
local ufw_status=$(ufw status | head -n1 | awk '{print $2}')
@@ -309,10 +364,628 @@ run_system_checks() {
check_system_requirements "$min_ram_gb" "$min_disk_gb" "$target_dir"
check_internet
check_docker_requirements
check_firewall
# Skip firewall check for macOS (handled differently)
if [[ "$OS_NAME_LOWER" != "macos" ]]; then
check_firewall
fi
# Check for container environment (warning only)
check_container_environment
success "All system checks passed"
}
# Get platform-specific default paths
get_default_media_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab/media"
else
echo "/mnt/media"
fi
}
get_default_config_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab/config"
else
echo "/opt/appdata"
fi
}
get_default_homelab_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab"
else
echo "/home/$USER/homelab"
fi
}
# Get Docker socket path for current platform
get_docker_socket_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/var/run/docker.sock"
else
echo "/var/run/docker.sock"
fi
}
# Package management abstraction
install_package() {
local package="$1"
if [[ -z "$package" ]]; then
error_exit "install_package requires a package name"
fi
info "📦 Installing package: $package"
case "$OS_NAME_LOWER" in
"macos")
if ! command_exists brew; then
error_exit "Homebrew not found. Please install Homebrew first: https://brew.sh/"
fi
# Get the actual user (not root) to run brew commands
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
sudo -u "$actual_user" brew install "$package"
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
apt-get update && apt-get install -y "$package"
;;
*)
error_exit "Unsupported OS for package installation: $OS_NAME"
;;
esac
success "Package installed: $package"
}
# Service management abstraction
start_service() {
local service="$1"
if [[ -z "$service" ]]; then
error_exit "start_service requires a service name"
fi
info "🚀 Starting service: $service"
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
# On macOS, Docker Desktop handles this
info "Docker Desktop should be started manually or via Docker Desktop app"
else
# Use launchctl for other services
launchctl start "$service" 2>/dev/null || true
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl start "$service"
;;
*)
error_exit "Unsupported OS for service management: $OS_NAME"
;;
esac
success "Service started: $service"
}
# Check if service is running
is_service_running() {
local service="$1"
if [[ -z "$service" ]]; then
return 1
fi
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
# Check if Docker daemon is responding
docker info >/dev/null 2>&1
else
# Check with launchctl
launchctl list | grep -q "$service"
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl is-active --quiet "$service"
;;
*)
return 1
;;
esac
}
# Enable service to start on boot
enable_service() {
local service="$1"
if [[ -z "$service" ]]; then
error_exit "enable_service requires a service name"
fi
info "⚙️ Enabling service: $service"
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
info "Docker Desktop auto-start should be configured in Docker Desktop settings"
else
# Use launchctl for other services
launchctl enable "$service" 2>/dev/null || true
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl enable "$service"
;;
*)
error_exit "Unsupported OS for service management: $OS_NAME"
;;
esac
success "Service enabled: $service"
}
# Get network interface IP address
get_primary_ip() {
local ip=""
case "$OS_NAME_LOWER" in
"macos")
# macOS network interface detection
ip=$(route get default | grep interface | awk '{print $2}' | head -1)
if [[ -n "$ip" ]]; then
ip=$(ifconfig "$ip" | grep 'inet ' | awk '{print $2}' | head -1)
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
# Linux network interface detection
ip=$(hostname -I | awk '{print $1}')
;;
*)
# Fallback method
ip=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'src \K\S+' | head -1)
;;
esac
# Validate IP address
if is_valid_ip "$ip"; then
echo "$ip"
else
echo "localhost"
fi
}
# Remove existing Docker installation on Linux
remove_docker_linux() {
info "🗑️ Removing existing Docker installation..."
# Stop Docker service if running
if systemctl is-active --quiet docker; then
info "🛑 Stopping Docker service..."
systemctl stop docker
fi
# Stop Docker socket if running
if systemctl is-active --quiet docker.socket; then
info "🛑 Stopping Docker socket..."
systemctl stop docker.socket
fi
# Disable Docker service
if systemctl is-enabled --quiet docker; then
info "🔧 Disabling Docker service..."
systemctl disable docker
fi
# Remove Docker packages
info "🗑️ Removing Docker packages..."
apt-get remove -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || true
apt-get purge -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || true
# Remove Docker Compose standalone if installed
if [[ -f "/usr/local/bin/docker-compose" ]]; then
info "🗑️ Removing Docker Compose standalone..."
rm -f "/usr/local/bin/docker-compose"
fi
# Remove Docker data directories
info "🗑️ Removing Docker data directories..."
local docker_dirs=(
"/var/lib/docker"
"/var/lib/containerd"
"/etc/docker"
"/etc/containerd"
"/run/docker"
"/run/containerd"
"/opt/containerd"
)
for dir in "${docker_dirs[@]}"; do
if [[ -d "$dir" ]]; then
rm -rf "$dir"
fi
done
# Remove Docker group
if getent group docker >/dev/null 2>&1; then
info "🗑️ Removing Docker group..."
groupdel docker 2>/dev/null || true
fi
# Remove Docker repository
if [[ -f "/etc/apt/sources.list.d/docker.list" ]]; then
info "🗑️ Removing Docker repository..."
rm -f "/etc/apt/sources.list.d/docker.list"
fi
# Remove Docker GPG key
if [[ -f "/etc/apt/keyrings/docker.gpg" ]]; then
rm -f "/etc/apt/keyrings/docker.gpg"
fi
# Remove any remaining Docker processes
pkill -f docker 2>/dev/null || true
pkill -f containerd 2>/dev/null || true
# Clean up package manager cache
apt-get autoremove -y 2>/dev/null || true
apt-get autoclean 2>/dev/null || true
success "Docker removal completed"
}
# Remove existing Docker installation on macOS
remove_docker_macos() {
info "🗑️ Removing existing Docker installation..."
# Get the actual user (not root) for operations
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Stop Docker Desktop if running
if pgrep -f "Docker Desktop" >/dev/null 2>&1; then
info "🛑 Stopping Docker Desktop..."
sudo -u "$actual_user" osascript -e 'quit app "Docker Desktop"' 2>/dev/null || true
sleep 3
fi
# Remove Docker Desktop application
if [[ -d "/Applications/Docker.app" ]]; then
info "🗑️ Removing Docker Desktop application..."
rm -rf "/Applications/Docker.app"
fi
# Remove Docker CLI tools installed via Homebrew
if command_exists brew; then
info "🗑️ Removing Docker via Homebrew..."
sudo -u "$actual_user" brew uninstall --cask docker 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-compose 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-machine 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-buildx 2>/dev/null || true
sudo -u "$actual_user" brew uninstall containerd 2>/dev/null || true
fi
# Remove Docker data directories
local docker_dirs=(
"/Users/$actual_user/.docker"
"/Users/$actual_user/Library/Preferences/com.docker.docker.plist"
"/Users/$actual_user/Library/Saved Application State/com.electron.docker-frontend.savedState"
"/Users/$actual_user/Library/Group Containers/group.com.docker"
"/Users/$actual_user/Library/Containers/com.docker.docker"
"/Users/$actual_user/Library/Application Support/Docker Desktop"
"/Users/$actual_user/Library/Logs/Docker Desktop"
"/Users/$actual_user/Library/Preferences/com.electron.docker-frontend.plist"
"/Users/$actual_user/Library/Caches/com.docker.docker"
)
for dir in "${docker_dirs[@]}"; do
if [[ -e "$dir" ]]; then
info "🗑️ Removing: $dir"
rm -rf "$dir"
fi
done
# Remove Docker symlinks and binaries
local docker_links=(
"/usr/local/bin/docker"
"/usr/local/bin/docker-compose"
"/usr/local/bin/docker-machine"
"/usr/local/bin/docker-buildx"
"/usr/local/bin/containerd"
"/usr/local/bin/containerd-shim"
"/usr/local/bin/containerd-shim-runc-v2"
"/usr/local/bin/ctr"
"/usr/local/bin/runc"
"/usr/local/bin/docker-credential-desktop"
"/usr/local/bin/docker-credential-ecr-login"
"/usr/local/bin/docker-credential-osxkeychain"
"/usr/local/bin/kubectl"
"/usr/local/bin/kubectl.docker"
"/usr/local/bin/vpnkit"
"/usr/local/bin/com.docker.cli"
)
for link in "${docker_links[@]}"; do
if [[ -L "$link" ]] || [[ -f "$link" ]]; then
info "🗑️ Removing: $link"
rm -f "$link"
fi
done
# Kill any remaining Docker processes
pkill -f docker 2>/dev/null || true
pkill -f com.docker 2>/dev/null || true
pkill -f containerd 2>/dev/null || true
success "Docker removal completed"
}
# Check existing Docker installation on macOS
check_existing_docker_macos() {
local docker_version=""
local docker_desktop_version=""
local installation_method=""
# Check for Docker command
if command_exists docker; then
docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
fi
# Check for Docker Desktop
if [[ -d "/Applications/Docker.app" ]]; then
docker_desktop_version=$(defaults read /Applications/Docker.app/Contents/Info.plist CFBundleShortVersionString 2>/dev/null || echo "unknown")
installation_method="Docker Desktop"
fi
# Check if installed via Homebrew
if command_exists brew; then
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
if sudo -u "$actual_user" brew list --cask docker >/dev/null 2>&1; then
installation_method="Homebrew Cask"
elif sudo -u "$actual_user" brew list docker >/dev/null 2>&1; then
installation_method="Homebrew"
fi
fi
# Return results
echo "docker_version=$docker_version"
echo "docker_desktop_version=$docker_desktop_version"
echo "installation_method=$installation_method"
}
# Install Docker for the current platform
install_docker() {
info "🐳 Installing Docker..."
case "$OS_NAME_LOWER" in
"macos")
# Check for existing Docker installation
local docker_info
docker_info=$(check_existing_docker_macos)
local docker_version=$(echo "$docker_info" | grep "docker_version=" | cut -d'=' -f2)
local docker_desktop_version=$(echo "$docker_info" | grep "docker_desktop_version=" | cut -d'=' -f2)
local installation_method=$(echo "$docker_info" | grep "installation_method=" | cut -d'=' -f2)
# If Docker is already installed, ask for confirmation to reinstall
if [[ -n "$docker_version" ]] || [[ -n "$docker_desktop_version" ]] || [[ -n "$installation_method" ]]; then
warning "Existing Docker installation detected:"
if [[ -n "$docker_version" ]]; then
info " Docker CLI version: $docker_version"
fi
if [[ -n "$docker_desktop_version" ]]; then
info " Docker Desktop version: $docker_desktop_version"
fi
if [[ -n "$installation_method" ]]; then
info " Installation method: $installation_method"
fi
echo
warning "⚠️ To ensure a clean HOPS installation, we recommend removing the existing Docker installation."
warning " This will remove all Docker data, containers, images, and volumes."
echo
read -p "❓ Do you want to remove the existing Docker installation and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_docker_macos
# Double-check removal was successful
sleep 2
if command_exists docker && docker info >/dev/null 2>&1; then
error_exit "Docker removal failed. Please manually remove Docker and try again."
fi
else
info "Keeping existing Docker installation. Checking if it's compatible..."
# Check if existing Docker is compatible
if ! docker info >/dev/null 2>&1; then
error_exit "Existing Docker installation is not running. Please start Docker Desktop manually or choose to reinstall."
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available in existing installation. Please reinstall Docker Desktop."
fi
success "Existing Docker installation is compatible"
return 0
fi
fi
# Install fresh Docker Desktop
info "📦 Installing Docker Desktop for Mac..."
# Check if Homebrew is available
if ! command_exists brew; then
warning "Homebrew not found. Installing Homebrew first..."
# Get the actual user (not root) to install Homebrew
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Install Homebrew as the actual user, not root
sudo -u "$actual_user" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add Homebrew to PATH for current session
if [[ -f "/opt/homebrew/bin/brew" ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [[ -f "/usr/local/bin/brew" ]]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
fi
# Install Docker Desktop via Homebrew Cask
info "📦 Installing Docker Desktop via Homebrew..."
# Get the actual user (not root) to run brew commands
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Remove conflicting compose-bridge binary if it exists
if [[ -f "/usr/local/bin/compose-bridge" ]]; then
info "🗑️ Removing conflicting compose-bridge binary..."
rm -f "/usr/local/bin/compose-bridge" 2>/dev/null || true
fi
sudo -u "$actual_user" brew install --cask docker
# Start Docker Desktop
info "🚀 Starting Docker Desktop..."
open -a Docker
# Wait for Docker to start
info "⏳ Waiting for Docker Desktop to start (this may take a few minutes)..."
local max_wait=120
local wait_time=0
while ! docker info >/dev/null 2>&1; do
if [[ $wait_time -ge $max_wait ]]; then
error_exit "Docker Desktop failed to start within $max_wait seconds. Please start it manually and try again."
fi
sleep 5
((wait_time += 5))
echo -n "."
done
echo
success "Docker Desktop installed and started successfully"
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
# Check for existing Docker installation
if command_exists docker; then
local docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
warning "Existing Docker installation detected:"
if [[ -n "$docker_version" ]]; then
info " Docker version: $docker_version"
fi
echo
warning "⚠️ To ensure a clean HOPS installation, we recommend removing the existing Docker installation."
warning " This will remove all Docker data, containers, images, and volumes."
echo
read -p "❓ Do you want to remove the existing Docker installation and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_docker_linux
# Double-check removal was successful
sleep 2
if command_exists docker && docker info >/dev/null 2>&1; then
error_exit "Docker removal failed. Please manually remove Docker and try again."
fi
else
info "Keeping existing Docker installation. Checking if it's compatible..."
# Check if existing Docker is compatible
if ! docker info >/dev/null 2>&1; then
error_exit "Existing Docker installation is not running. Please start Docker service manually or choose to reinstall."
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available in existing installation. Please reinstall Docker."
fi
success "Existing Docker installation is compatible"
return 0
fi
fi
# Install fresh Docker using the official script
info "📦 Installing Docker Engine..."
curl -fsSL https://get.docker.com | sh
# Add user to docker group if we're running with sudo
if [[ -n "$SUDO_USER" ]]; then
usermod -aG docker "$SUDO_USER"
fi
# Start and enable Docker service
start_service docker
enable_service docker
success "Docker installed and configured"
;;
*)
error_exit "Unsupported OS for Docker installation: $OS_NAME"
;;
esac
}
# Check if Docker is properly installed and running
check_docker_installation() {
info "🐳 Checking Docker installation..."
# Check if Docker command exists
if ! command_exists docker; then
return 1
fi
# Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
return 1
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
return 1
fi
success "Docker is properly installed and running"
return 0
}
+74 -26
View File
@@ -21,6 +21,30 @@ get_linuxserver_env() {
EOF
}
# Get timezone mount path for current platform
get_timezone_mount() {
if [[ "$(uname -s)" == "Darwin" ]]; then
# macOS doesn't need timezone mount, use TZ environment variable
echo ""
else
# Linux timezone mount
echo "$(get_timezone_mount)"
fi
}
# Get GPU device access for current platform
get_gpu_devices() {
if [[ "$(uname -s)" == "Darwin" ]]; then
# macOS doesn't support GPU passthrough to Docker containers
echo ""
else
# Linux GPU device access
cat <<EOF
$(get_gpu_devices)
EOF
fi
}
# Common restart policy
get_restart_policy() {
echo " restart: unless-stopped"
@@ -65,7 +89,7 @@ $(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/sonarr:/config
- \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 8989)
$(get_homelab_network)
labels:
@@ -91,7 +115,7 @@ $(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/radarr:/config
- \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 7878)
$(get_homelab_network)
labels:
@@ -117,7 +141,7 @@ $(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/lidarr:/config
- \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 8686)
$(get_homelab_network)
labels:
@@ -143,7 +167,7 @@ $(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/readarr:/config
- \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 8787)
$(get_homelab_network)
labels:
@@ -169,7 +193,7 @@ $(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/bazarr:/config
- \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 6767)
$(get_homelab_network)
labels:
@@ -194,7 +218,7 @@ $(get_restart_policy)
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/prowlarr:/config
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 9696)
$(get_homelab_network)
labels:
@@ -229,8 +253,7 @@ $(get_linuxserver_env)
- \${CONFIG_ROOT}/tdarr/logs:/app/logs
- \${DATA_ROOT}/media:/media
- \${DATA_ROOT}/downloads/tdarr:/temp
devices:
- /dev/dri:/dev/dri # Intel GPU
$(get_gpu_devices)
$(get_web_healthcheck 8265)
$(get_homelab_network)
labels:
@@ -243,6 +266,33 @@ $(get_homelab_network)
EOF
}
generate_huntarr() {
cat <<EOF
huntarr:
image: ghcr.io/plexguide/huntarr:latest
container_name: huntarr
$(get_restart_policy)
ports:
- "9705:9705"
environment:
$(get_linuxserver_env)
- BASE_URL=\${BASE_URL:-}
volumes:
- \${CONFIG_ROOT}/huntarr:/config
- \${DATA_ROOT}:/data
$(get_timezone_mount)
$(get_web_healthcheck 9705 "/health")
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.huntarr.rule=Host(\`huntarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.huntarr.entrypoints=websecure"
- "traefik.http.routers.huntarr.tls.certresolver=letsencrypt"
- "traefik.http.services.huntarr.loadbalancer.server.port=9705"
EOF
}
# --------------------------------------------
# DOWNLOAD CLIENTS
# --------------------------------------------
@@ -263,7 +313,7 @@ $(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/qbittorrent:/config
- \${DATA_ROOT}/downloads/torrents:/data/torrents
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 8082)
$(get_homelab_network)
labels:
@@ -294,7 +344,7 @@ $(get_linuxserver_env)
- \${CONFIG_ROOT}/transmission:/config
- \${DATA_ROOT}/downloads/torrents:/data/torrents
- \${DATA_ROOT}/downloads/torrents/watch:/watch
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 9091 "/transmission/web/")
$(get_homelab_network)
labels:
@@ -320,7 +370,7 @@ $(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/nzbget:/config
- \${DATA_ROOT}/downloads/usenet:/data/usenet
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 6789)
$(get_homelab_network)
labels:
@@ -346,7 +396,7 @@ $(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/sabnzbd:/config
- \${DATA_ROOT}/downloads/usenet:/data/usenet
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 8080)
$(get_homelab_network)
labels:
@@ -380,9 +430,8 @@ $(get_restart_policy)
- \${CONFIG_ROOT}/jellyfin:/config
- \${CONFIG_ROOT}/jellyfin/cache:/cache
- \${DATA_ROOT}/media:/media:ro
- /etc/localtime:/etc/localtime:ro
devices:
- /dev/dri:/dev/dri # Intel GPU
$(get_timezone_mount)
$(get_gpu_devices)
group_add:
- "109" # render group for GPU access
$(get_web_healthcheck 8096 "/health")
@@ -425,9 +474,8 @@ $(get_restart_policy)
- \${CONFIG_ROOT}/plex:/config
- \${CONFIG_ROOT}/plex/transcode:/transcode
- \${DATA_ROOT}/media:/data:ro
- /etc/localtime:/etc/localtime:ro
devices:
- /dev/dri:/dev/dri # Intel GPU
$(get_timezone_mount)
$(get_gpu_devices)
$(get_web_healthcheck 32400 "/identity")
$(get_homelab_network)
labels:
@@ -456,9 +504,8 @@ $(get_restart_policy)
volumes:
- \${CONFIG_ROOT}/emby:/config
- \${DATA_ROOT}/media:/data:ro
- /etc/localtime:/etc/localtime:ro
devices:
- /dev/dri:/dev/dri # Intel GPU
$(get_timezone_mount)
$(get_gpu_devices)
$(get_web_healthcheck 8096)
$(get_homelab_network)
labels:
@@ -538,7 +585,7 @@ $(get_restart_policy)
- TZ=\${TZ}
volumes:
- \${CONFIG_ROOT}/overseerr:/app/config
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 5055)
$(get_homelab_network)
labels:
@@ -564,7 +611,7 @@ $(get_restart_policy)
- TZ=\${TZ}
volumes:
- \${CONFIG_ROOT}/jellyseerr:/app/config
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 5055)
$(get_homelab_network)
labels:
@@ -589,7 +636,7 @@ $(get_restart_policy)
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/ombi:/config
- /etc/localtime:/etc/localtime:ro
$(get_timezone_mount)
$(get_web_healthcheck 3579)
$(get_homelab_network)
labels:
@@ -850,6 +897,7 @@ generate_service_definition() {
"bazarr") generate_bazarr ;;
"prowlarr") generate_prowlarr ;;
"tdarr") generate_tdarr ;;
"huntarr") generate_huntarr ;;
# Download Clients
"qbittorrent") generate_qbittorrent ;;
@@ -968,7 +1016,7 @@ entryPoints:
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
endpoint: "unix://$(get_docker_socket_path)"
exposedByDefault: false
file:
directory: /etc/traefik/dynamic
@@ -1266,7 +1314,7 @@ show_usage() {
HOPS Service Definitions Script v3.1.0
Usage:
source hops_service_definitions.sh
source services
generate_hops_stack service1 service2 service3...
Examples:
+5 -5
View File
@@ -26,7 +26,7 @@ fi
# Phase 1: Privileged setup
info "📋 Phase 1: Privileged setup (requires root)"
if "$SCRIPT_DIR/hops_privileged_setup.sh"; then
if "$SCRIPT_DIR/privileged-setup"; then
success "Privileged setup completed"
else
error_exit "Privileged setup failed"
@@ -61,7 +61,7 @@ case "$choice" in
;;
4)
echo "Available services:"
"$SCRIPT_DIR/hops_service_definitions_improved.sh" list
"$SCRIPT_DIR/services-improved" list
read -p "Enter service names (space-separated): " -a services
;;
*)
@@ -71,10 +71,10 @@ case "$choice" in
esac
# Generate and deploy
if "$SCRIPT_DIR/hops_user_operations.sh" generate "${services[@]}"; then
if "$SCRIPT_DIR/user-operations" generate "${services[@]}"; then
echo "Configuration generated successfully"
if "$SCRIPT_DIR/hops_user_operations.sh" deploy; then
if "$SCRIPT_DIR/user-operations" deploy; then
echo "Services deployed successfully"
else
echo "Deployment failed"
@@ -87,4 +87,4 @@ fi
USERSCRIPT
success "Installation completed successfully"
success "Services are now running. Check status with: ./hops_user_operations.sh status"
success "Services are now running. Check status with: ./user-operations status"
View File
+1 -1
View File
@@ -45,7 +45,7 @@ EOF
# Generate service definitions
for service in "${services[@]}"; do
if "$SCRIPT_DIR/hops_service_definitions_improved.sh" generate "$service" >> "$compose_file"; then
if "$SCRIPT_DIR/services-improved" generate "$service" >> "$compose_file"; then
success "Added service: $service"
else
error_exit "Failed to generate service definition for: $service"