From 5affcd2e262d8dc079efc22c077917894b5eb50b Mon Sep 17 00:00:00 2001 From: Stephen Klein Date: Thu, 17 Jul 2025 21:52:22 -0400 Subject: [PATCH] Rename scripts for clarity and add Huntarr service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CLAUDE.md | 137 +- README.md | 33 +- fixed_installer.sh | 925 ------------ fixed_service_definitions.sh | 1285 ----------------- fixed_primary_script.sh => hops | 40 +- hops.sh | 679 --------- hops_uninstaller_fixed.sh | 560 ------- hops_installer_enhanced.sh => install | 241 ++-- lib/common.sh | 26 +- lib/privileges.sh | 18 +- lib/system.sh | 699 ++++++++- hops_privileged_setup.sh => privileged-setup | 0 hops_service_definitions.sh => services | 100 +- ...finitions_improved.sh => services-improved | 0 hops_install.sh => setup | 10 +- fixed_uninstaller.sh => uninstall | 0 hops_user_operations.sh => user-operations | 2 +- 17 files changed, 1077 insertions(+), 3678 deletions(-) delete mode 100755 fixed_installer.sh delete mode 100755 fixed_service_definitions.sh rename fixed_primary_script.sh => hops (95%) delete mode 100755 hops.sh delete mode 100755 hops_uninstaller_fixed.sh rename hops_installer_enhanced.sh => install (83%) rename hops_privileged_setup.sh => privileged-setup (100%) rename hops_service_definitions.sh => services (94%) rename hops_service_definitions_improved.sh => services-improved (100%) rename hops_install.sh => setup (85%) rename fixed_uninstaller.sh => uninstall (100%) rename hops_user_operations.sh => user-operations (97%) diff --git a/CLAUDE.md b/CLAUDE.md index ff44a8d..6ac724f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` \ No newline at end of file +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 \ No newline at end of file diff --git a/README.md b/README.md index 566eba2..c09278e 100644 --- a/README.md +++ b/README.md @@ -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 # Run as user -./hops_user_operations.sh deploy # Run as user +sudo ./privileged-setup # Run as root +./user-operations generate # 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 # 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 # 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 diff --git a/fixed_installer.sh b/fixed_installer.sh deleted file mode 100755 index 449d03b..0000000 --- a/fixed_installer.sh +++ /dev/null @@ -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" </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/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 -} - \ No newline at end of file diff --git a/fixed_service_definitions.sh b/fixed_service_definitions.sh deleted file mode 100755 index f4da945..0000000 --- a/fixed_service_definitions.sh +++ /dev/null @@ -1,1285 +0,0 @@ -#!/bin/bash - -# HOPS Service Definitions -# Contains all Docker Compose service configurations -# Version: 3.1.0 - -# This script provides functions to generate Docker Compose service definitions -# Usage: Source this script and call generate_service_definition - -# -------------------------------------------- -# COMMON CONFIGURATIONS -# -------------------------------------------- - -# Common environment variables for LinuxServer containers -get_linuxserver_env() { - cat < "$compose_file" <> "$compose_file" - if generate_service_definition "$service" >> "$compose_file"; then - echo "✅ Added service: $service" - else - echo "⚠️ Failed to add service: $service" - fi - done - - echo "📝 Docker Compose file generated with ${#services[@]} services" -} - -# Create service-specific configuration directories and files -create_service_configs() { - local services=("$@") - local config_root="${CONFIG_ROOT:-/opt/appdata}" - - for service in "${services[@]}"; do - local service_dir="$config_root/$service" - mkdir -p "$service_dir" - - case "$service" in - "portainer") - # Create admin password file for Portainer - if [[ -n "${DEFAULT_ADMIN_PASSWORD}" ]] && command -v htpasswd &>/dev/null; then - echo -n "${DEFAULT_ADMIN_PASSWORD}" | htpasswd -niB admin | cut -d: -f2 > "$service_dir/admin_password" - fi - ;; - "traefik") - mkdir -p "$service_dir/letsencrypt" - mkdir -p "$service_dir/dynamic" - # Create basic traefik config - cat > "$service_dir/traefik.yml" < "$service_dir/redis.conf" < "$service_dir/init/create_databases.sql" < "$service_dir/configuration.yml" < "$service_dir/users_database.yml" </dev/null || true - fi - done -} - -# Get service dependencies -get_service_dependencies() { - local service="$1" - - case "$service" in - "jellystat") - echo "postgres" - ;; - "authelia") - echo "redis" - ;; - *) - # No dependencies - ;; - esac -} - -# Get all dependencies for a list of services -resolve_dependencies() { - local services=("$@") - local all_services=() - local processed=() - - # Add services and their dependencies - for service in "${services[@]}"; do - if [[ ! " ${processed[*]} " =~ " ${service} " ]]; then - all_services+=("$service") - processed+=("$service") - - # Add dependencies - local deps=$(get_service_dependencies "$service") - for dep in $deps; do - if [[ ! " ${processed[*]} " =~ " ${dep} " ]]; then - all_services+=("$dep") - processed+=("$dep") - fi - done - fi - done - - echo "${all_services[@]}" -} - -# Get service ports for conflict checking -get_service_ports() { - local service="$1" - - case "$service" in - "sonarr") echo "8989" ;; - "radarr") echo "7878" ;; - "lidarr") echo "8686" ;; - "readarr") echo "8787" ;; - "bazarr") echo "6767" ;; - "prowlarr") echo "9696" ;; - "tdarr") echo "8265 8266" ;; - "qbittorrent") echo "8082 6881 6881/udp" ;; - "transmission") echo "9091 51413 51413/udp" ;; - "nzbget") echo "6789" ;; - "sabnzbd") echo "8080" ;; - "jellyfin") echo "8096 8920 7359/udp 1900/udp" ;; - "plex") echo "32400 1900/udp 3005 5353/udp 8324 32410/udp 32412/udp 32413/udp 32414/udp 32469" ;; - "emby") echo "8097 8920" ;; - "jellystat") echo "3000" ;; - "overseerr") echo "5055" ;; - "jellyseerr") echo "5056" ;; - "ombi") echo "3579" ;; - "traefik") echo "80 443 8080" ;; - "nginx-proxy-manager") echo "80 443 81" ;; - "authelia") echo "9091" ;; - "portainer") echo "9000 9443" ;; - "uptime-kuma") echo "3001" ;; - "postgres") echo "5432" ;; - "redis") echo "6379" ;; - *) echo "" ;; - esac -} - -# Print service summary -print_service_summary() { - local services=("$@") - - echo "===========================" - echo "HOPS SERVICE SUMMARY" - echo "===========================" - echo "Selected services: ${#services[@]}" - echo - - # Categorize services - local media_mgmt=() - local download_clients=() - local media_servers=() - local request_mgmt=() - local proxy_security=() - local monitoring=() - local databases=() - - for service in "${services[@]}"; do - case "$service" in - sonarr|radarr|lidarr|readarr|bazarr|prowlarr|tdarr) - media_mgmt+=("$service") ;; - qbittorrent|transmission|nzbget|sabnzbd) - download_clients+=("$service") ;; - jellyfin|plex|emby|jellystat) - media_servers+=("$service") ;; - overseerr|jellyseerr|ombi) - request_mgmt+=("$service") ;; - traefik|nginx-proxy-manager|authelia) - proxy_security+=("$service") ;; - portainer|watchtower|uptime-kuma) - monitoring+=("$service") ;; - postgres|redis) - databases+=("$service") ;; - esac - done - - [[ ${#media_mgmt[@]} -gt 0 ]] && echo "📺 Media Management: ${media_mgmt[*]}" - [[ ${#download_clients[@]} -gt 0 ]] && echo "⬇️ Download Clients: ${download_clients[*]}" - [[ ${#media_servers[@]} -gt 0 ]] && echo "🎞️ Media Servers: ${media_servers[*]}" - [[ ${#request_mgmt[@]} -gt 0 ]] && echo "🎛️ Request Management: ${request_mgmt[*]}" - [[ ${#proxy_security[@]} -gt 0 ]] && echo "🔒 Proxy & Security: ${proxy_security[*]}" - [[ ${#monitoring[@]} -gt 0 ]] && echo "📈 Monitoring: ${monitoring[*]}" - [[ ${#databases[@]} -gt 0 ]] && echo "🗄️ Databases: ${databases[*]}" - - echo -} - -# Main function to generate everything -generate_hops_stack() { - local services=("$@") - - if [[ ${#services[@]} -eq 0 ]]; then - echo "Error: No services specified" - return 1 - fi - - echo "Generating HOPS stack for: ${services[*]}" - - # Resolve dependencies - local all_services=($(resolve_dependencies "${services[@]}")) - - echo "Services with dependencies: ${all_services[*]}" - - # Print summary - print_service_summary "${all_services[@]}" - - # Generate docker-compose.yml - generate_complete_compose "${all_services[@]}" - - # Create service configurations - create_service_configs "${all_services[@]}" - - echo "HOPS stack generation complete!" -} - -# Helper function to list all available services -list_available_services() { - echo "Available HOPS services:" - echo - echo "📺 MEDIA MANAGEMENT:" - echo " sonarr radarr lidarr readarr bazarr prowlarr tdarr" - echo - echo "⬇️ DOWNLOAD CLIENTS:" - echo " qbittorrent transmission nzbget sabnzbd" - echo - echo "🎞️ MEDIA SERVERS:" - echo " jellyfin plex emby jellystat" - echo - echo "🎛️ REQUEST MANAGEMENT:" - echo " overseerr jellyseerr ombi" - echo - echo "🔒 PROXY & SECURITY:" - echo " traefik nginx-proxy-manager authelia" - echo - echo "📈 MONITORING:" - echo " portainer watchtower uptime-kuma" - echo - echo "🗄️ DATABASES:" - echo " postgres redis" -} - -# Usage information -show_usage() { - cat < - Generate single service - generate_complete_compose - Generate docker-compose.yml - create_service_configs - Create config directories - list_available_services - Show all available services - resolve_dependencies - Add required dependencies - get_service_ports - Get service port mappings - -EOF -} \ No newline at end of file diff --git a/fixed_primary_script.sh b/hops similarity index 95% rename from fixed_primary_script.sh rename to hops index 64a66af..2a3f822 100755 --- a/fixed_primary_script.sh +++ b/hops @@ -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" diff --git a/hops.sh b/hops.sh deleted file mode 100755 index 64a66af..0000000 --- a/hops.sh +++ /dev/null @@ -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 \ No newline at end of file diff --git a/hops_uninstaller_fixed.sh b/hops_uninstaller_fixed.sh deleted file mode 100755 index 709678c..0000000 --- a/hops_uninstaller_fixed.sh +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/hops_installer_enhanced.sh b/install similarity index 83% rename from hops_installer_enhanced.sh rename to install index 0bed53c..0a5192e 100755 --- a/hops_installer_enhanced.sh +++ b/install @@ -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." - 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." + # 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 + + # 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 @@ -921,4 +929,7 @@ EOF log "🎉 HOPS Enhanced deployment completed successfully!" return 0 -} \ No newline at end of file +} + +# Execute the main installation function +install_hops \ No newline at end of file diff --git a/lib/common.sh b/lib/common.sh index ad31507..5ed690a 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -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 diff --git a/lib/privileges.sh b/lib/privileges.sh index eae2893..0344e60 100755 --- a/lib/privileges.sh +++ b/lib/privileges.sh @@ -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") diff --git a/lib/system.sh b/lib/system.sh index 48dfd16..5c7a9c6 100644 --- a/lib/system.sh +++ b/lib/system.sh @@ -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 } \ No newline at end of file diff --git a/hops_privileged_setup.sh b/privileged-setup similarity index 100% rename from hops_privileged_setup.sh rename to privileged-setup diff --git a/hops_service_definitions.sh b/services similarity index 94% rename from hops_service_definitions.sh rename to services index f4da945..8ca9207 100755 --- a/hops_service_definitions.sh +++ b/services @@ -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 <> "$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"