commit 772fb7da52cdb31806c3d9289a93649ac69453f1 Author: Stephen Klein Date: Sat Jul 12 06:42:48 2025 -0400 Initial release of HOPS v3.1.0 - Complete homelab orchestration and provisioning system - Support for 20+ popular homelab services - Interactive installation with dependency resolution - Security hardening and firewall configuration - Service health monitoring and management interface - Comprehensive error handling with rollback capabilities - Complete uninstaller with data preservation options ๐Ÿค– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab9cf5b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 HOPS Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f2d3c27 --- /dev/null +++ b/README.md @@ -0,0 +1,339 @@ +# HOPS - Homelab Orchestration Provisioning Script + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Version](https://img.shields.io/badge/Version-3.1.0-blue.svg)]() +[![Platform](https://img.shields.io/badge/Platform-Ubuntu%2FDebian%2FMint-orange.svg)]() + +**HOPS** is a comprehensive, automated deployment solution for popular homelab applications. It simplifies the process of setting up and managing Docker-based services including media servers, download clients, monitoring tools, and more. + +## ๐ŸŽฏ What is HOPS? + +HOPS (Homelab Orchestration Provisioning Script) automates the deployment of a complete homelab infrastructure using Docker Compose. It provides an intuitive menu-driven interface for selecting, configuring, and managing services with enterprise-grade features like: + +- **Automated dependency resolution** +- **Security hardening and firewall configuration** +- **Service health monitoring** +- **Rollback capabilities on failure** +- **Comprehensive logging** +- **User-friendly management interface** + +## โœจ Key Features + +### ๐Ÿš€ **Easy Installation** +- One-command installation process +- Automatic Docker installation and configuration +- Interactive service selection +- Intelligent dependency resolution + +### ๐Ÿ”’ **Security First** +- Automatic firewall configuration +- Secure password generation +- File permission hardening +- Network isolation + +### ๐Ÿ“Š **Management & Monitoring** +- Real-time service status monitoring +- Centralized log viewing +- Easy service management (start/stop/restart) +- Health checks and service verification + +### ๐Ÿ”„ **Reliability** +- Error handling with automatic rollback +- Service dependency management +- Port conflict detection +- System requirements validation + +## ๐Ÿ“ฑ Supported Services + +### ๐Ÿ“บ Media Management (*arr Stack) +- **Sonarr** - TV show management +- **Radarr** - Movie management +- **Lidarr** - Music management +- **Readarr** - eBook/audiobook management +- **Bazarr** - Subtitle management +- **Prowlarr** - Indexer management +- **Tdarr** - Media transcoding + +### โฌ‡๏ธ Download Clients +- **qBittorrent** - Feature-rich BitTorrent client +- **Transmission** - Lightweight BitTorrent client +- **NZBGet** - Efficient Usenet downloader +- **SABnzbd** - Popular Usenet client + +### ๐ŸŽž๏ธ Media Servers +- **Jellyfin** - Open-source media server +- **Plex** - Popular media server platform +- **Emby** - Feature-rich media server +- **Jellystat** - Jellyfin statistics and monitoring + +### ๐ŸŽ›๏ธ Request Management +- **Overseerr** - Media request management for Plex +- **Jellyseerr** - Media request management for Jellyfin +- **Ombi** - Media request platform + +### ๐Ÿ”’ Reverse Proxy & Security +- **Traefik** - Modern reverse proxy with automatic SSL +- **Nginx Proxy Manager** - Easy-to-use reverse proxy +- **Authelia** - Authentication and authorization server + +### ๐Ÿ“ˆ Monitoring & Management +- **Portainer** - Docker container management +- **Uptime Kuma** - Service monitoring +- **Watchtower** - Automatic container updates + +## ๐Ÿ”ง System Requirements + +### Minimum Requirements +- **OS**: Ubuntu 20.04+, Debian 11+, or Linux Mint 20+ +- **RAM**: 2GB (4GB+ recommended) +- **Storage**: 10GB free space (more for media) +- **CPU**: 2 cores recommended +- **Network**: Internet connection required + +### Prerequisites +- Root/sudo access +- x86_64 architecture + +## ๐Ÿš€ Quick Start + +### 1. Download HOPS +```bash +git clone https://github.com/yourusername/hops.git +cd hops +chmod +x hops.sh +``` + +### 2. Run Installation +```bash +sudo ./hops.sh +``` + +### 3. Follow the Interactive Setup +- Select your desired services +- Configure directories and timezone +- Choose security options +- Wait for automated deployment + +### 4. Access Your Services +The installer will provide URLs for all deployed services: +``` +๐Ÿ“ฑ Access your services at: + โ— Jellyfin http://192.168.1.100:8096 + โ— Sonarr http://192.168.1.100:8989 + โ— Radarr http://192.168.1.100:7878 + โ— Portainer http://192.168.1.100:9000 +``` + +## ๐Ÿ“ Default Directory Structure + +``` +~/homelab/ # Main homelab directory +โ”œโ”€โ”€ docker-compose.yml # Service definitions +โ”œโ”€โ”€ .env # Environment variables +โ””โ”€โ”€ logs/ # Application logs + +/opt/appdata/ # Application configurations +โ”œโ”€โ”€ jellyfin/ +โ”œโ”€โ”€ sonarr/ +โ”œโ”€โ”€ radarr/ +โ””โ”€โ”€ ... + +/mnt/media/ # Media storage +โ”œโ”€โ”€ movies/ +โ”œโ”€โ”€ tv/ +โ”œโ”€โ”€ music/ +โ””โ”€โ”€ downloads/ +``` + +## ๐ŸŽ›๏ธ Management Interface + +HOPS includes a comprehensive management interface accessible through the main script: + +```bash +sudo ./hops.sh +``` + +### Available Options: +1. **Install HOPS** - Deploy new services +2. **Uninstall HOPS** - Complete removal with options +3. **Manage Services** - Start/stop/restart services +4. **Service Status** - Real-time service monitoring +5. **Access Information** - Get service URLs and credentials +6. **View Logs** - Centralized log viewing +7. **Help & Documentation** - Built-in help system + +## ๐Ÿ”ง Advanced Configuration + +### Environment Variables +All configuration is stored in `~/homelab/.env`: + +```bash +# Core Configuration +PUID=1000 # User ID +PGID=1000 # Group ID +TZ=America/New_York # Timezone + +# Directory Configuration +DATA_ROOT=/mnt/media # Media storage +CONFIG_ROOT=/opt/appdata # App configurations + +# Security +DEFAULT_ADMIN_PASSWORD=... # Generated secure password +DEFAULT_DB_PASSWORD=... # Database password + +# Optional: Custom domain +DOMAIN=yourdomain.com +ACME_EMAIL=admin@yourdomain.com +``` + +### Service Management Commands +```bash +# Navigate to homelab directory +cd ~/homelab + +# View running services +docker compose ps + +# View logs +docker compose logs -f [service-name] + +# Restart specific service +docker compose restart [service-name] + +# Update all services +docker compose pull && docker compose up -d + +# Stop all services +docker compose down +``` + +## ๐Ÿ”’ Security Features + +### Automatic Security Hardening +- **Firewall Configuration**: Automatic UFW rules for service ports +- **Secure Passwords**: Cryptographically secure password generation +- **File Permissions**: Restrictive permissions on sensitive files +- **Network Isolation**: Docker network segregation +- **SSL/TLS**: Automatic certificate management with Traefik + +### Post-Installation Security +1. **Change Default Passwords**: Update passwords in `.env` file +2. **Configure Reverse Proxy**: Set up Traefik or Nginx Proxy Manager +3. **Enable Authentication**: Configure Authelia for additional security +4. **Regular Updates**: Use Watchtower for automatic updates + +## ๐Ÿ†˜ Troubleshooting + +### Common Issues + +#### Port Conflicts +```bash +# Check for port conflicts +sudo lsof -i :PORT_NUMBER + +# View HOPS service status +sudo ./hops.sh +# Select option 4: Service Status +``` + +#### Service Won't Start +```bash +# Check service logs +cd ~/homelab +docker compose logs [service-name] + +# Restart service +docker compose restart [service-name] +``` + +#### Permission Issues +```bash +# Fix ownership of data directories +sudo chown -R $USER:$USER /mnt/media /opt/appdata +``` + +### Log Locations +- **Installation Logs**: `/var/log/hops/` +- **Service Logs**: `docker compose logs [service-name]` +- **System Logs**: `journalctl -u docker` + +### Getting Help +1. Check the built-in help: `sudo ./hops.sh` โ†’ Option 7 +2. Review logs in `/var/log/hops/` +3. Verify Docker status: `systemctl status docker` +4. Check service health: `docker compose ps` + +## ๐Ÿ”„ Backup and Recovery + +### Backup Important Data +```bash +# Backup configurations +sudo tar -czf hops-config-backup.tar.gz /opt/appdata + +# Backup compose files +cp ~/homelab/.env ~/homelab/docker-compose.yml /backup/location/ +``` + +### Recovery +```bash +# Restore configurations +sudo tar -xzf hops-config-backup.tar.gz -C / + +# Redeploy services +cd ~/homelab +docker compose up -d +``` + +## ๐Ÿ“Š Performance Tuning + +### For Low-Resource Systems +- Start with fewer services initially +- Monitor resource usage with Portainer +- Consider using lightweight alternatives (Transmission vs qBittorrent) + +### For High-Performance Systems +- Enable GPU transcoding in Jellyfin/Plex +- Use SSD storage for application data +- Configure multiple download clients for redundancy + +## ๐Ÿค Contributing + +We welcome contributions! Please: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Test thoroughly +5. Submit a pull request + +### Development Setup +```bash +git clone https://github.com/yourusername/hops.git +cd hops +# Make changes to scripts +# Test with: sudo ./hops.sh +``` + +## ๐Ÿ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## ๐Ÿ™ Acknowledgments + +- **LinuxServer.io** for excellent Docker images +- **Docker** for containerization platform +- **The Servarr Team** for the *arr applications +- **Jellyfin Project** for the open-source media server +- All the amazing open-source projects that make HOPS possible + +## ๐Ÿ“ž Support + +- **Documentation**: Check this README and built-in help +- **Issues**: Report bugs via GitHub Issues +- **Community**: Join discussions in GitHub Discussions + +--- + +**Made with โค๏ธ for the homelab community** + +*HOPS - Making homelab deployment simple, secure, and reliable.* \ No newline at end of file diff --git a/fixed_installer.sh b/fixed_installer.sh new file mode 100755 index 0000000..449d03b --- /dev/null +++ b/fixed_installer.sh @@ -0,0 +1,925 @@ +#!/bin/bash + +install_hops() { + # Clear terminal at startup + clear + + # Exit on any error + set -e + + # Script version for update tracking + local SCRIPT_VERSION="3.1.0" + local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + # -------------------------------------------- + # LOGGING SETUP + # -------------------------------------------- + local LOG_DIR="/var/log/hops" + local LOG_FILE="$LOG_DIR/homelab-setup-$(date +%Y%m%d-%H%M%S).log" + mkdir -p "$LOG_DIR" + touch "$LOG_FILE" + + log() { + echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE" + } + + error_exit() { + log "โŒ ERROR: $1" + log "โŒ Installation failed. Check logs at: $LOG_FILE" + exit 1 + } + + # Enhanced error handling with rollback + DEPLOYMENT_STEPS_COMPLETED=() + + track_step() { + DEPLOYMENT_STEPS_COMPLETED+=("$1") + log "โœ… Step completed: $1" + } + + rollback_deployment() { + log "๐Ÿ”„ Rolling back deployment..." + + for step in "${DEPLOYMENT_STEPS_COMPLETED[@]}"; do + case "$step" in + "containers_started") + log "๐Ÿ›‘ Stopping containers..." + docker compose down --timeout 30 2>/dev/null || true + ;; + "images_pulled") + log "๐Ÿ—‘๏ธ Removing pulled images..." + docker compose down --rmi all 2>/dev/null || true + ;; + "directories_created") + log "๐Ÿ“ Cleaning up directories..." + [[ -n "$APPDATA_DIR" ]] && rm -rf "$APPDATA_DIR" 2>/dev/null || true + ;; + "compose_generated") + log "๐Ÿ“ Removing compose file..." + [[ -f "docker-compose.yml" ]] && rm -f docker-compose.yml + ;; + esac + done + + log "๐Ÿ”„ Rollback completed" + } + + error_exit_with_rollback() { + log "โŒ ERROR: $1" + rollback_deployment + log "โŒ Installation failed and rolled back. Check logs at: $LOG_FILE" + exit 1 + } + + # -------------------------------------------- + # HEADER + # -------------------------------------------- + cat << "EOF" + + _ _ ____ ____ ____ + | | | || _ \| _ \/ ___| + | |__| || |_) | |_) \___ \ + | __ || __/| __/ ___) | + |_| |_||_| |_| |____/ + +EOF + echo -e "๐Ÿš€ Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}\n" + log "๐Ÿš€ Starting HOPS Deployment v${SCRIPT_VERSION}" + + # -------------------------------------------- + # SYSTEM REQUIREMENTS CHECK + # -------------------------------------------- + check_system_requirements() { + local MIN_RAM_GB=2 + local MIN_DISK_GB=10 + local MIN_CORES=2 + + log "๐Ÿ” Checking system requirements..." + + # Check RAM + local RAM_GB=$(free -g | awk '/^Mem:/{print $2}') + if [[ $RAM_GB -lt $MIN_RAM_GB ]]; then + error_exit "Insufficient RAM: ${RAM_GB}GB detected, ${MIN_RAM_GB}GB required" + fi + + # Check disk space + local DISK_AVAIL=$(df -BG --output=avail / | tail -n 1 | tr -d 'G') + if [[ $DISK_AVAIL -lt $MIN_DISK_GB ]]; then + error_exit "Insufficient disk space: ${DISK_AVAIL}GB available, ${MIN_DISK_GB}GB required" + fi + + # Check CPU cores + local CPU_CORES=$(nproc) + if [[ $CPU_CORES -lt $MIN_CORES ]]; then + log "โš ๏ธ Low CPU cores: ${CPU_CORES} detected, ${MIN_CORES} recommended" + fi + + log "โœ… System meets minimum requirements (${RAM_GB}GB RAM, ${CPU_CORES} cores, ${DISK_AVAIL}GB disk)" + } + + # -------------------------------------------- + # REQUIRED PACKAGES CHECK + # -------------------------------------------- + check_required_packages() { + local missing_packages=() + local required_packages=("curl" "wget" "openssl" "lsof" "apache2-utils") + + log "๐Ÿ“ฆ Checking required packages..." + + for package in "${required_packages[@]}"; do + if ! command -v "${package%%-*}" &>/dev/null; then + missing_packages+=("$package") + fi + done + + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log "๐Ÿ“ฆ Installing missing packages: ${missing_packages[*]}" + apt-get update && apt-get install -y "${missing_packages[@]}" + fi + + log "โœ… All required packages are installed" + } + + # -------------------------------------------- + # ROOT CHECK + # -------------------------------------------- + if [[ $EUID -ne 0 ]]; then + error_exit "This script must be run as root or with sudo." + fi + + # -------------------------------------------- + # OS DETECTION + # -------------------------------------------- + detect_os() { + if command -v lsb_release &>/dev/null; then + OS_NAME=$(lsb_release -is) + OS_VERSION=$(lsb_release -rs) + else + OS_NAME=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"') + OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"') + fi + OS_NAME_LOWER=$(echo "$OS_NAME" | tr '[:upper:]' '[:lower:]') + + if [[ ! "$OS_NAME_LOWER" =~ ^(ubuntu|debian|linuxmint|mint)$ ]]; then + error_exit "Unsupported OS: $OS_NAME Only Debian/Ubuntu/Mint supported" + fi + log "โœ… Detected OS: $OS_NAME $OS_VERSION" + } + + # -------------------------------------------- + # USER CONFIGURATION COLLECTION + # -------------------------------------------- + collect_user_configuration() { + log "๐Ÿ”ง Collecting user configuration..." + + # Get running user info + if [[ -n "$SUDO_USER" ]]; then + RUNNING_USER="$SUDO_USER" + PUID=$(id -u "$SUDO_USER") + PGID=$(id -g "$SUDO_USER") + else + RUNNING_USER="root" + PUID=1000 + PGID=1000 + log "โš ๏ธ Running as root, defaulting to PUID=1000, PGID=1000" + fi + + # Timezone configuration + echo -e "\n๐ŸŒ Timezone Configuration" + echo "Current timezone: $(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown")" + echo -e "Keep current timezone? [Y/n]: " + read -r keep_tz + + if [[ "$keep_tz" =~ ^[Nn]$ ]]; then + echo -e "Enter timezone (e.g., America/New_York, Europe/London): " + read -r user_timezone + validate_timezone "$user_timezone" + TIMEZONE="$user_timezone" + else + TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "America/New_York") + fi + + # Directory configuration + echo -e "\n๐Ÿ“ Directory Configuration" + echo -e "Media directory [/mnt/media]: " + read -r media_dir + MEDIA_DIR="${media_dir:-/mnt/media}" + + echo -e "Application data directory [/opt/appdata]: " + read -r appdata_dir + APPDATA_DIR="${appdata_dir:-/opt/appdata}" + + # Create directories + mkdir -p "$MEDIA_DIR"/{movies,tv,music,books,downloads} + mkdir -p "$APPDATA_DIR" + + # Set ownership if not root + if [[ "$RUNNING_USER" != "root" ]]; then + chown -R "$PUID:$PGID" "$MEDIA_DIR" "$APPDATA_DIR" 2>/dev/null || true + fi + + log "โœ… User configuration collected" + log " User: $RUNNING_USER ($PUID:$PGID)" + log " Timezone: $TIMEZONE" + log " Media: $MEDIA_DIR" + log " AppData: $APPDATA_DIR" + } + + # -------------------------------------------- + # VALIDATION FUNCTIONS + # -------------------------------------------- + validate_timezone() { + if ! timedatectl list-timezones | grep -qx "$1" 2>/dev/null; then + log "โš ๏ธ Timezone '$1' invalid, defaulting to 'America/New_York'" + TIMEZONE="America/New_York" + fi + } + + validate_password() { + local password="$1" + local min_length="${2:-12}" + + if [[ -z "$password" ]]; then + echo -e "\n๐Ÿ” Password must meet these requirements:" + echo " โ€ข Minimum $min_length characters" + echo " โ€ข At least one uppercase letter" + echo " โ€ข At least one lowercase letter" + echo " โ€ข At least one number" + echo " โ€ข At least one special character" + return 3 + fi + + if [[ ${#password} -lt $min_length ]]; then + return 1 + fi + + if [[ ! "$password" =~ [A-Z] ]] || [[ ! "$password" =~ [a-z] ]] || \ + [[ ! "$password" =~ [0-9] ]] || [[ ! "$password" =~ [^A-Za-z0-9] ]]; then + return 2 + fi + + return 0 + } + + check_port() { + local PORT=$1 + local SERVICE=$2 + if lsof -i :"$PORT" >/dev/null 2>&1; then + local PROCESS=$(lsof -ti :"$PORT" | head -1) + local PROCESS_NAME=$(ps -p "$PROCESS" -o comm= 2>/dev/null || echo "unknown") + log "โš ๏ธ Port $PORT is already in use by $PROCESS_NAME. $SERVICE may fail to start." + return 1 + fi + return 0 + } + + check_all_ports() { + local SERVICES=("$@") + local CONFLICTS=() + + # Source service definitions to get port mappings + if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then + source "$SCRIPT_DIR/hops_service_definitions.sh" + fi + + for svc in "${SERVICES[@]}"; do + local ports=$(get_service_ports "$svc") + for port in $ports; do + if ! check_port "$port" "$svc"; then + CONFLICTS+=("Port $port ($svc)") + fi + done + done + + if [[ ${#CONFLICTS[@]} -gt 0 ]]; then + log "โš ๏ธ Found ${#CONFLICTS[@]} port conflicts:" + for conflict in "${CONFLICTS[@]}"; do + log " โ€ข $conflict" + done + + echo -e "\nโš ๏ธ Port conflicts detected! Continue anyway? (y/N): " + read -r continue_choice + if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then + error_exit "Installation cancelled due to port conflicts." + fi + return 1 + fi + return 0 + } + + # -------------------------------------------- + # DOCKER COMPOSE VERSION CHECK + # -------------------------------------------- + check_docker_compose_version() { + # Check for Docker Compose plugin (v2) + if docker compose version &>/dev/null; then + log "โœ… Docker Compose plugin detected ($(docker compose version --short))" + return 0 + fi + + # Check for standalone docker-compose (v1) + if command -v docker-compose &>/dev/null; then + log "โš ๏ธ Found legacy docker-compose (v1). Installing Docker Compose plugin..." + if ! apt-get install -y docker-compose-plugin 2>&1 | tee -a "$LOG_FILE"; then + error_exit "Failed to install Docker Compose plugin." + fi + return 0 + fi + + # Neither found + error_exit "No Docker Compose detected. Please install Docker first." + } + + # -------------------------------------------- + # IMPROVED PASSWORD GENERATION + # -------------------------------------------- + generate_secure_password() { + local length="${1:-16}" + local max_attempts=5 + local attempt=1 + + while [[ $attempt -le $max_attempts ]]; do + # Generate password with mixed case, numbers, and symbols + local password=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-${length}) + + # Ensure it meets complexity requirements + if validate_password "$password" "$length"; then + echo "$password" + return 0 + fi + + ((attempt++)) + done + + # Fallback: construct a guaranteed compliant password + local upper=$(tr -dc 'A-Z' < /dev/urandom | head -c2) + local lower=$(tr -dc 'a-z' < /dev/urandom | head -c4) + local digits=$(tr -dc '0-9' < /dev/urandom | head -c2) + local symbols=$(tr -dc '!@#$%^&*' < /dev/urandom | head -c2) + local remaining_length=$((length - 10)) + + if [[ $remaining_length -gt 0 ]]; then + local remaining=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c$remaining_length) + echo "${upper}${lower}${digits}${symbols}${remaining}" | fold -w1 | shuf | tr -d '\n' + else + echo "${upper}${lower}${digits}${symbols}" + fi + } + + # -------------------------------------------- + # ENVIRONMENT FILE GENERATION + # -------------------------------------------- + create_env_file() { + local homelab_dir="$1" + + log "๐Ÿ“ Creating environment file..." + + cat > "$homelab_dir/.env" </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_primary_script.sh b/fixed_primary_script.sh new file mode 100755 index 0000000..64a66af --- /dev/null +++ b/fixed_primary_script.sh @@ -0,0 +1,679 @@ +#!/bin/bash + +# HOPS - Homelab Orchestration Provisioning Script +# Primary Management Script +# Version: 3.1.0 + +# Exit on any error +set -e + +# Script version and metadata +readonly SCRIPT_VERSION="3.1.0" +readonly SCRIPT_NAME="HOPS" +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Default script locations +readonly INSTALLER_SCRIPT="$SCRIPT_DIR/hops_installer_enhanced.sh" +readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/hops_uninstaller_fixed.sh" +readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/hops_service_definitions.sh" + +# Color codes for output +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly PURPLE='\033[0;35m' +readonly CYAN='\033[0;36m' +readonly WHITE='\033[1;37m' +readonly NC='\033[0m' # No Color + +# Logging setup +readonly LOG_DIR="/var/log/hops" +readonly LOG_FILE="$LOG_DIR/hops-main-$(date +%Y%m%d-%H%M%S).log" + +# Initialize logging +init_logging() { + if [[ $EUID -eq 0 ]]; then + mkdir -p "$LOG_DIR" + touch "$LOG_FILE" + fi +} + +# Logging function +log() { + local message="$1" + local timestamp="$(date '+%Y-%m-%d %T')" + + if [[ -w "$LOG_FILE" ]]; then + echo "$timestamp - $message" >> "$LOG_FILE" + fi + + echo -e "$message" +} + +# Error handling +error_exit() { + log "${RED}โŒ ERROR: $1${NC}" + exit 1 +} + +# Warning function +warning() { + log "${YELLOW}โš ๏ธ WARNING: $1${NC}" +} + +# Success function +success() { + log "${GREEN}โœ… $1${NC}" +} + +# Info function +info() { + log "${BLUE}โ„น๏ธ $1${NC}" +} + +# Clear screen and show header +show_header() { + clear + cat << "EOF" + + _ _ ____ ____ ____ + | | | || _ \| _ \/ ___| + | |__| || |_) | |_) \___ \ + | __ || __/| __/ ___) | + |_| |_||_| |_| |____/ + +EOF + echo -e "${CYAN}๐Ÿš€ Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}${NC}" + echo -e "${WHITE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}\n" +} + +# Check if running as root +check_root() { + if [[ $EUID -ne 0 ]]; then + error_exit "This script must be run as root or with sudo." + fi +} + +# Validate script dependencies +check_dependencies() { + local missing_deps=() + + # Check for required scripts + if [[ ! -f "$INSTALLER_SCRIPT" ]]; then + missing_deps+=("Installer script: $INSTALLER_SCRIPT") + fi + + if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then + missing_deps+=("Uninstaller script: $UNINSTALLER_SCRIPT") + fi + + if [[ ! -f "$SERVICE_DEFINITIONS" ]]; then + missing_deps+=("Service definitions: $SERVICE_DEFINITIONS") + fi + + # Check for required commands + local required_commands=("curl" "wget" "systemctl") + for cmd in "${required_commands[@]}"; do + if ! command -v "$cmd" &>/dev/null; then + missing_deps+=("Command: $cmd") + fi + done + + if [[ ${#missing_deps[@]} -gt 0 ]]; then + error_exit "Missing dependencies:\n$(printf ' โ€ข %s\n' "${missing_deps[@]}")" + fi +} + +# Check system requirements +check_system_requirements() { + info "Checking system requirements..." + + # Check OS + if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then + warning "This script is designed for Ubuntu/Debian/Mint systems" + echo -e "Continue anyway? [y/N]: " + read -r continue_choice + [[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 0 + fi + + # Check minimum requirements + local ram_gb=$(free -g | awk '/^Mem:/{print $2}') + local disk_gb=$(df -BG --output=avail / | tail -n 1 | tr -d 'G') + + if [[ $ram_gb -lt 2 ]]; then + warning "Low RAM detected: ${ram_gb}GB (2GB+ recommended)" + fi + + if [[ $disk_gb -lt 10 ]]; then + warning "Low disk space: ${disk_gb}GB (10GB+ recommended)" + fi + + success "System requirements check complete" +} + +# Get HOPS installation status +get_installation_status() { + local status="not_installed" + local homelab_dirs=( + "$HOME/homelab" + "/home/*/homelab" + "/opt/homelab" + "/srv/homelab" + ) + + # Check for existing installation + for dir in "${homelab_dirs[@]}"; do + if [[ -f "$dir/docker-compose.yml" ]]; then + status="installed" + HOMELAB_DIR="$dir" + break + fi + done + + # Check for running containers + if command -v docker &>/dev/null && docker ps --format "{{.Names}}" | grep -qE "(sonarr|radarr|jellyfin|plex|portainer)"; then + if [[ "$status" == "not_installed" ]]; then + status="partial" + else + status="running" + fi + fi + + echo "$status" +} + +# Show installation status +show_status() { + local status=$(get_installation_status) + + echo -e "${WHITE}๐Ÿ“Š Current Status:${NC}" + case "$status" in + "not_installed") + echo -e " ${RED}โ— Not Installed${NC}" + ;; + "installed") + echo -e " ${YELLOW}โ— Installed (stopped)${NC}" + echo -e " ${BLUE}๐Ÿ“‚ Location: $HOMELAB_DIR${NC}" + ;; + "running") + echo -e " ${GREEN}โ— Running${NC}" + echo -e " ${BLUE}๐Ÿ“‚ Location: $HOMELAB_DIR${NC}" + ;; + "partial") + echo -e " ${YELLOW}โ— Partial Installation${NC}" + ;; + esac + echo +} + +# Run installer +run_installer() { + info "Launching HOPS installer..." + + if [[ ! -f "$INSTALLER_SCRIPT" ]]; then + error_exit "Installer script not found: $INSTALLER_SCRIPT" + fi + + # Source the installer function and run it + if source "$INSTALLER_SCRIPT" && install_hops; then + success "Installation completed successfully!" + else + error_exit "Installation failed. Check logs for details." + fi + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Run uninstaller +run_uninstaller() { + info "Launching HOPS uninstaller..." + + if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then + error_exit "Uninstaller script not found: $UNINSTALLER_SCRIPT" + fi + + # Source the uninstaller function and run it + if source "$UNINSTALLER_SCRIPT" && uninstall_hops; then + success "Uninstallation completed successfully!" + else + error_exit "Uninstallation failed. Check logs for details." + fi + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Show service status +show_service_status() { + show_header + echo -e "${WHITE}๐Ÿ” Service Status Check${NC}\n" + + if ! command -v docker &>/dev/null; then + warning "Docker is not installed" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + local status=$(get_installation_status) + if [[ "$status" == "not_installed" ]]; then + warning "HOPS is not installed" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + echo -e "${BLUE}๐Ÿ“Š Docker Service Status:${NC}" + if systemctl is-active --quiet docker; then + echo -e " ${GREEN}โ— Docker daemon: Running${NC}" + else + echo -e " ${RED}โ— Docker daemon: Stopped${NC}" + fi + + echo -e "\n${BLUE}๐Ÿ“ฆ Container Status:${NC}" + + # Source service definitions to get port info + if [[ -f "$SERVICE_DEFINITIONS" ]]; then + source "$SERVICE_DEFINITIONS" + fi + + # Known HOPS services with their ports + local services=( + "sonarr:8989" "radarr:7878" "lidarr:8686" "readarr:8787" + "bazarr:6767" "prowlarr:9696" "jellyfin:8096" "plex:32400" + "overseerr:5055" "jellyseerr:5056" "portainer:9000" + "traefik:8080" "nginx-proxy-manager:81" "qbittorrent:8082" + "transmission:9091" "nzbget:6789" "sabnzbd:8080" + "uptime-kuma:3001" "jellystat:3000" + ) + + local running_count=0 + local total_count=0 + + for service_info in "${services[@]}"; do + local service_name="${service_info%:*}" + local service_port="${service_info#*:}" + + if docker ps --format "{{.Names}}" | grep -q "^${service_name}$"; then + ((total_count++)) + local status_symbol="${GREEN}โ—${NC}" + local status_text="Running" + + # Check if port is accessible + if curl -sSf --max-time 2 --connect-timeout 1 "http://localhost:${service_port}" >/dev/null 2>&1; then + status_text="Running & Accessible" + ((running_count++)) + else + status_text="Running (starting up)" + status_symbol="${YELLOW}โ—${NC}" + fi + + printf " %s %-20s %s (:%s)\n" "$status_symbol" "$service_name" "$status_text" "$service_port" + elif docker ps -a --format "{{.Names}}" | grep -q "^${service_name}$"; then + ((total_count++)) + printf " %s %-20s %s\n" "${RED}โ—${NC}" "$service_name" "Stopped" + fi + done + + if [[ $total_count -eq 0 ]]; then + echo -e " ${YELLOW}No HOPS services found${NC}" + else + echo -e "\n${WHITE}๐Ÿ“ˆ Summary: ${running_count}/${total_count} services running and accessible${NC}" + fi + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Manage services (start/stop/restart) +manage_services() { + show_header + echo -e "${WHITE}๐ŸŽ›๏ธ Service Management${NC}\n" + + local status=$(get_installation_status) + if [[ "$status" == "not_installed" ]]; then + warning "HOPS is not installed" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + if [[ -z "$HOMELAB_DIR" ]]; then + error_exit "Cannot locate homelab directory with docker-compose.yml" + fi + + echo -e "${BLUE}Available actions:${NC}" + echo -e " 1) Start all services" + echo -e " 2) Stop all services" + echo -e " 3) Restart all services" + echo -e " 4) View logs (recent)" + echo -e " 5) View logs (follow)" + echo -e " 6) Update services" + echo -e " 7) Restart individual service" + echo -e " 8) Back to main menu" + + echo -e "\n${WHITE}Select an option [1-8]: ${NC}" + read -r choice + + cd "$HOMELAB_DIR" + + case $choice in + 1) + info "Starting all services..." + if docker compose up -d; then + success "Services started" + else + warning "Some services may have failed to start" + fi + ;; + 2) + info "Stopping all services..." + if docker compose down; then + success "Services stopped" + else + warning "Some services may not have stopped cleanly" + fi + ;; + 3) + info "Restarting all services..." + if docker compose restart; then + success "Services restarted" + else + warning "Some services may have failed to restart" + fi + ;; + 4) + info "Showing recent logs..." + docker compose logs --tail=100 + ;; + 5) + info "Following logs (Ctrl+C to exit)..." + docker compose logs -f --tail=50 + ;; + 6) + info "Updating services..." + if docker compose pull && docker compose up -d; then + success "Services updated" + else + warning "Update may have failed" + fi + ;; + 7) + echo -e "\n${WHITE}Available services:${NC}" + docker compose ps --services | nl -w2 -s') ' + echo -e "\n${WHITE}Enter service name to restart: ${NC}" + read -r service_name + if [[ -n "$service_name" ]]; then + info "Restarting $service_name..." + if docker compose restart "$service_name"; then + success "$service_name restarted" + else + warning "Failed to restart $service_name" + fi + fi + ;; + 8) + return + ;; + *) + warning "Invalid option" + ;; + esac + + echo -e "\n${WHITE}Press Enter to continue...${NC}" + read -r +} + +# Show quick access URLs +show_access_info() { + show_header + echo -e "${WHITE}๐ŸŒ Service Access Information${NC}\n" + + local status=$(get_installation_status) + if [[ "$status" == "not_installed" ]]; then + warning "HOPS is not installed" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + echo -e "${BLUE}๐Ÿ“ฑ Access your services at:${NC}" + + # Get local IP + local local_ip=$(hostname -I | awk '{print $1}') + + # Service URLs with paths + local services=( + "Sonarr:8989:/sonarr" + "Radarr:7878:/radarr" + "Lidarr:8686:/lidarr" + "Readarr:8787:/readarr" + "Bazarr:6767:" + "Prowlarr:9696:" + "Jellyfin:8096:" + "Plex:32400:/web" + "Overseerr:5055:" + "Jellyseerr:5056:" + "Portainer:9000:" + "Traefik:8080:" + "NPM:81:" + "qBittorrent:8082:" + "Transmission:9091:" + "NZBGet:6789:" + "SABnzbd:8080:" + "Uptime-Kuma:3001:" + "Jellystat:3000:" + ) + + local active_services=0 + for service_info in "${services[@]}"; do + local service_name="${service_info%%:*}" + local service_port="${service_info#*:}" + local service_path="${service_port#*:}" + service_port="${service_port%:*}" + + if docker ps --format "{{.Names}}" | grep -qi "${service_name,,}"; then + local url="http://${local_ip}:${service_port}${service_path}" + printf " ${GREEN}โ—${NC} %-15s %s\n" "$service_name" "$url" + ((active_services++)) + fi + done + + if [[ $active_services -eq 0 ]]; then + echo -e " ${YELLOW}No services currently running${NC}" + fi + + echo -e "\n${YELLOW}๐Ÿ’ก Tips:${NC}" + echo -e " โ€ข Bookmark these URLs for easy access" + echo -e " โ€ข Default credentials are in the .env file" + echo -e " โ€ข Change default passwords after first login" + echo -e " โ€ข Some services may take a few minutes to fully start" + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Show logs +show_logs() { + show_header + echo -e "${WHITE}๐Ÿ“‹ HOPS Logs${NC}\n" + + if [[ ! -d "$LOG_DIR" ]]; then + warning "No log directory found" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + echo -e "${BLUE}Available log files:${NC}" + local log_files=($(find "$LOG_DIR" -name "*.log" -type f | sort -r)) + + if [[ ${#log_files[@]} -eq 0 ]]; then + warning "No log files found" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + local count=1 + for log_file in "${log_files[@]}"; do + local basename_log=$(basename "$log_file") + local size=$(du -h "$log_file" | cut -f1) + local date=$(stat -c %y "$log_file" | cut -d' ' -f1) + + printf " %d) %-40s (%s, %s)\n" "$count" "$basename_log" "$size" "$date" + ((count++)) + done + + echo -e "\n${WHITE}Select a log file to view [1-${#log_files[@]}] or 0 to go back: ${NC}" + read -r choice + + if [[ "$choice" -eq 0 ]]; then + return + elif [[ "$choice" -gt 0 && "$choice" -le ${#log_files[@]} ]]; then + local selected_log="${log_files[$((choice-1))]}" + echo -e "\n${BLUE}Showing last 50 lines of $(basename "$selected_log"):${NC}\n" + tail -50 "$selected_log" + else + warning "Invalid selection" + fi + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Show help information +show_help() { + show_header + echo -e "${WHITE}๐Ÿ“š HOPS Help & Documentation${NC}\n" + + echo -e "${BLUE}๐ŸŽฏ What is HOPS?${NC}" + echo -e "HOPS (Homelab Orchestration Provisioning Script) is an automated installer" + echo -e "for popular homelab applications including media servers, download clients," + echo -e "and monitoring tools.\n" + + echo -e "${BLUE}๐Ÿš€ Quick Start:${NC}" + echo -e " 1. Run this script as root/sudo" + echo -e " 2. Choose 'Install HOPS' from the menu" + echo -e " 3. Configure directories and timezone" + echo -e " 4. Select your desired services" + echo -e " 5. Wait for installation to complete" + echo -e " 6. Access services via the provided URLs\n" + + echo -e "${BLUE}๐Ÿ“ฑ Supported Services:${NC}" + echo -e " โ€ข Media Management: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr" + echo -e " โ€ข Download Clients: qBittorrent, Transmission, NZBGet, SABnzbd" + echo -e " โ€ข Media Servers: Jellyfin, Plex, Emby, Jellystat" + echo -e " โ€ข Request Management: Overseerr, Jellyseerr, Ombi" + echo -e " โ€ข Reverse Proxy: Traefik, Nginx Proxy Manager" + echo -e " โ€ข Monitoring: Portainer, Uptime Kuma, Watchtower\n" + + echo -e "${BLUE}๐Ÿ”ง Requirements:${NC}" + echo -e " โ€ข Ubuntu/Debian/Mint Linux" + echo -e " โ€ข 2GB+ RAM (4GB+ recommended)" + echo -e " โ€ข 10GB+ free disk space" + echo -e " โ€ข Root/sudo access" + echo -e " โ€ข Internet connection\n" + + echo -e "${BLUE}๐Ÿ“ Default Locations:${NC}" + echo -e " โ€ข Homelab directory: ~/homelab/" + echo -e " โ€ข App configurations: /opt/appdata/" + echo -e " โ€ข Media storage: /mnt/media/" + echo -e " โ€ข Logs: /var/log/hops/\n" + + echo -e "${BLUE}๐Ÿ†˜ Troubleshooting:${NC}" + echo -e " โ€ข Check logs in the 'View Logs' menu" + echo -e " โ€ข Verify Docker is running: systemctl status docker" + echo -e " โ€ข Check container status: docker ps" + echo -e " โ€ข View service logs: docker logs [service-name]" + echo -e " โ€ข Restart services: docker compose restart [service-name]\n" + + echo -e "${BLUE}๐Ÿ” Security Notes:${NC}" + echo -e " โ€ข Change default passwords in .env file after installation" + echo -e " โ€ข Configure firewall rules as needed" + echo -e " โ€ข Regularly update services using the management menu\n" + + echo -e "${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Main menu +show_main_menu() { + local status=$(get_installation_status) + + echo -e "${WHITE}๐ŸŽ›๏ธ Main Menu:${NC}" + echo -e " 1) Install HOPS" + + if [[ "$status" != "not_installed" ]]; then + echo -e " 2) Uninstall HOPS" + echo -e " 3) Manage Services" + echo -e " 4) Service Status" + echo -e " 5) Access Information" + else + echo -e " 2) Uninstall HOPS ${YELLOW}(not installed)${NC}" + echo -e " 3) Manage Services ${YELLOW}(not installed)${NC}" + echo -e " 4) Service Status ${YELLOW}(not installed)${NC}" + echo -e " 5) Access Information ${YELLOW}(not installed)${NC}" + fi + + echo -e " 6) View Logs" + echo -e " 7) Help & Documentation" + echo -e " 8) Exit" + + echo -e "\n${WHITE}Select an option [1-8]: ${NC}" +} + +# Main program loop +main() { + init_logging + check_root + check_dependencies + + while true; do + show_header + show_status + show_main_menu + + read -r choice + + case $choice in + 1) + check_system_requirements + run_installer + ;; + 2) + run_uninstaller + ;; + 3) + manage_services + ;; + 4) + show_service_status + ;; + 5) + show_access_info + ;; + 6) + show_logs + ;; + 7) + show_help + ;; + 8) + info "Thank you for using HOPS!" + exit 0 + ;; + *) + warning "Invalid option. Please select 1-8." + sleep 2 + ;; + esac + done +} + +# Script entry point +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/fixed_service_definitions.sh b/fixed_service_definitions.sh new file mode 100755 index 0000000..f4da945 --- /dev/null +++ b/fixed_service_definitions.sh @@ -0,0 +1,1285 @@ +#!/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_uninstaller.sh b/fixed_uninstaller.sh new file mode 100755 index 0000000..709678c --- /dev/null +++ b/fixed_uninstaller.sh @@ -0,0 +1,560 @@ +#!/bin/bash + +uninstall_hops() { + # Clear terminal at startup + clear + + # Exit on any error (but allow some failures during cleanup) + set +e + + # Script version for consistency + local SCRIPT_VERSION="3.1.0" + + # -------------------------------------------- + # LOGGING SETUP + # -------------------------------------------- + local LOG_DIR="/var/log/hops" + local LOG_FILE="$LOG_DIR/homelab-uninstall-$(date +%Y%m%d-%H%M%S).log" + mkdir -p "$LOG_DIR" + touch "$LOG_FILE" + + log() { + echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE" + } + + error_exit() { + log "โŒ ERROR: $1" + log "โŒ Uninstallation failed. Check logs at: $LOG_FILE" + exit 1 + } + + warning() { + log "โš ๏ธ WARNING: $1" + } + + # -------------------------------------------- + # HEADER + # -------------------------------------------- + cat << "EOF" + + _ _ ____ ____ ____ + | | | || _ \| _ \/ ___| + | |__| || |_) | |_) \___ \ + | __ || __/| __/ ___) | + |_| |_||_| |_| |____/ + +EOF + echo -e "๐Ÿ—‘๏ธ Homelab Orchestration Provisioning Script - UNINSTALLER v${SCRIPT_VERSION}\n" + log "๐Ÿ—‘๏ธ Starting HOPS Uninstallation v${SCRIPT_VERSION}" + + # -------------------------------------------- + # ROOT CHECK + # -------------------------------------------- + if [[ $EUID -ne 0 ]]; then + error_exit "This script must be run as root or with sudo." + fi + + # -------------------------------------------- + # CONFIRMATION PROMPT + # -------------------------------------------- + show_uninstall_warning() { + echo -e "โš ๏ธ WARNING: This will completely remove your HOPS installation!" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo -e "This uninstaller will:" + echo -e " โ€ข Stop and remove all Docker containers" + echo -e " โ€ข Remove Docker images (optional)" + echo -e " โ€ข Remove Docker Compose configuration" + echo -e " โ€ข Clean up application data (optional)" + echo -e " โ€ข Remove firewall rules" + echo -e " โ€ข Uninstall Docker (optional)" + echo -e "" + echo -e "โš ๏ธ YOUR MEDIA FILES WILL NOT BE DELETED" + echo -e "โš ๏ธ APPLICATION DATA REMOVAL IS OPTIONAL" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + } + + get_uninstall_options() { + echo -e "\n๐Ÿ”ง Uninstall Options:" + + # Container and compose removal (always done) + REMOVE_CONTAINERS=true + REMOVE_COMPOSE=true + + # Optional removals + echo -e "\nโ“ Remove Docker images? (saves disk space but requires re-download) [y/N]: " + read -r remove_images + REMOVE_IMAGES=false + [[ "$remove_images" =~ ^[Yy]$ ]] && REMOVE_IMAGES=true + + echo -e "\nโ“ Remove application data? (โš ๏ธ DELETES ALL CONFIGURATIONS!) [y/N]: " + read -r remove_appdata + REMOVE_APPDATA=false + [[ "$remove_appdata" =~ ^[Yy]$ ]] && REMOVE_APPDATA=true + + echo -e "\nโ“ Uninstall Docker completely? [y/N]: " + read -r remove_docker + REMOVE_DOCKER=false + [[ "$remove_docker" =~ ^[Yy]$ ]] && REMOVE_DOCKER=true + + echo -e "\nโ“ Remove firewall rules? [Y/n]: " + read -r remove_firewall + REMOVE_FIREWALL=true + [[ "$remove_firewall" =~ ^[Nn]$ ]] && REMOVE_FIREWALL=false + + # Final confirmation + echo -e "\nโš ๏ธ FINAL CONFIRMATION" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo -e "Actions to perform:" + echo -e " โ€ข Remove containers: โœ…" + echo -e " โ€ข Remove compose files: โœ…" + [[ $REMOVE_IMAGES == true ]] && echo -e " โ€ข Remove Docker images: โœ…" || echo -e " โ€ข Remove Docker images: โŒ" + [[ $REMOVE_APPDATA == true ]] && echo -e " โ€ข Remove app data: โœ…" || echo -e " โ€ข Remove app data: โŒ" + [[ $REMOVE_DOCKER == true ]] && echo -e " โ€ข Uninstall Docker: โœ…" || echo -e " โ€ข Uninstall Docker: โŒ" + [[ $REMOVE_FIREWALL == true ]] && echo -e " โ€ข Remove firewall rules: โœ…" || echo -e " โ€ข Remove firewall rules: โŒ" + + echo -e "\nโ“ Proceed with uninstallation? [y/N]: " + read -r final_confirm + if [[ ! "$final_confirm" =~ ^[Yy]$ ]]; then + log "๐Ÿšซ Uninstallation cancelled by user" + exit 0 + fi + } + + # -------------------------------------------- + # HOMELAB DIRECTORY DETECTION + # -------------------------------------------- + find_homelab_directory() { + local POSSIBLE_DIRS=( + "$HOME/homelab" + "/home/*/homelab" + "/opt/homelab" + "/srv/homelab" + ) + + # Try to find from running user's home first + if [[ -n "$SUDO_USER" ]]; then + local user_home=$(eval echo "~$SUDO_USER") + POSSIBLE_DIRS=("$user_home/homelab" "${POSSIBLE_DIRS[@]}") + fi + + HOMELAB_DIR="" + for dir in "${POSSIBLE_DIRS[@]}"; do + if [[ -f "$dir/docker-compose.yml" ]]; then + HOMELAB_DIR="$dir" + log "โœ… Found homelab directory: $HOMELAB_DIR" + break + fi + done + + if [[ -z "$HOMELAB_DIR" ]]; then + echo -e "\n๐Ÿ“‚ Could not auto-detect homelab directory." + echo -e "Please enter the path to your homelab directory (contains docker-compose.yml):" + read -r user_dir + + if [[ -f "$user_dir/docker-compose.yml" ]]; then + HOMELAB_DIR="$user_dir" + log "โœ… Using homelab directory: $HOMELAB_DIR" + else + warning "No docker-compose.yml found in $user_dir" + log "๐Ÿ“ Will proceed with container cleanup by name instead" + fi + fi + + # Set APPDATA_DIR from env file if available + if [[ -f "$HOMELAB_DIR/.env" ]]; then + APPDATA_DIR=$(grep "^CONFIG_ROOT=" "$HOMELAB_DIR/.env" | cut -d= -f2) + log "๐Ÿ“ Found appdata directory: $APPDATA_DIR" + fi + } + + # -------------------------------------------- + # SERVICE DETECTION + # -------------------------------------------- + detect_running_services() { + log "๐Ÿ” Detecting running HOPS services..." + + # Known HOPS service names + local KNOWN_SERVICES=( + "sonarr" "radarr" "lidarr" "readarr" "bazarr" "prowlarr" "tdarr" + "nzbget" "sabnzbd" "transmission" "qbittorrent" + "plex" "jellyfin" "emby" "jellystat" "jellystat-db" + "overseerr" "jellyseerr" "ombi" + "traefik" "nginx-proxy-manager" "authelia" + "portainer" "watchtower" "uptime-kuma" + "postgres" "redis" + ) + + DETECTED_SERVICES=() + for service in "${KNOWN_SERVICES[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${service}$"; then + DETECTED_SERVICES+=("$service") + fi + done + + if [[ ${#DETECTED_SERVICES[@]} -gt 0 ]]; then + log "โœ… Detected services: ${DETECTED_SERVICES[*]}" + else + log "โš ๏ธ No HOPS services detected" + fi + } + + # -------------------------------------------- + # CONTAINER CLEANUP + # -------------------------------------------- + stop_and_remove_containers() { + log "๐Ÿ›‘ Stopping and removing containers..." + + if [[ -n "$HOMELAB_DIR" && -f "$HOMELAB_DIR/docker-compose.yml" ]]; then + cd "$HOMELAB_DIR" + + # Stop services gracefully + log "๐Ÿ”„ Stopping services with docker compose..." + if docker compose ps -q | grep -q .; then + if ! docker compose down --timeout 30 2>&1 | tee -a "$LOG_FILE"; then + warning "Docker compose down failed, attempting force removal" + docker compose down --timeout 10 --remove-orphans --volumes 2>&1 | tee -a "$LOG_FILE" || true + fi + else + log "โ„น๏ธ No running compose services found" + fi + fi + + # Fallback: Remove containers by name + if [[ ${#DETECTED_SERVICES[@]} -gt 0 ]]; then + log "๐Ÿงน Cleaning up remaining containers..." + for service in "${DETECTED_SERVICES[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${service}$"; then + log "๐Ÿ—‘๏ธ Removing container: $service" + docker stop "$service" 2>/dev/null || true + docker rm -f "$service" 2>/dev/null || true + fi + done + fi + + log "โœ… Container cleanup complete" + } + + # -------------------------------------------- + # IMAGE CLEANUP + # -------------------------------------------- + remove_docker_images() { + if [[ $REMOVE_IMAGES == true ]]; then + log "๐Ÿ—‘๏ธ Removing Docker images..." + + # Common HOPS images + local HOPS_IMAGES=( + "lscr.io/linuxserver/sonarr" + "lscr.io/linuxserver/radarr" + "lscr.io/linuxserver/lidarr" + "lscr.io/linuxserver/readarr" + "lscr.io/linuxserver/bazarr" + "lscr.io/linuxserver/prowlarr" + "ghcr.io/haveagitgat/tdarr" + "lscr.io/linuxserver/nzbget" + "lscr.io/linuxserver/sabnzbd" + "lscr.io/linuxserver/transmission" + "lscr.io/linuxserver/qbittorrent" + "plexinc/pms-docker" + "jellyfin/jellyfin" + "emby/embyserver" + "sctx/overseerr" + "fallenbagel/jellyseerr" + "lscr.io/linuxserver/ombi" + "traefik" + "jc21/nginx-proxy-manager" + "authelia/authelia" + "portainer/portainer-ce" + "containrrr/watchtower" + "louislam/uptime-kuma" + "postgres" + "redis" + "cyfershepard/jellystat" + ) + + for image in "${HOPS_IMAGES[@]}"; do + if docker images --format "{{.Repository}}" | grep -q "^${image}$"; then + log "๐Ÿ—‘๏ธ Removing image: $image" + docker rmi -f "$image" 2>/dev/null || true + fi + done + + # Clean up dangling images + log "๐Ÿงน Cleaning up dangling images..." + docker image prune -f 2>/dev/null || true + + log "โœ… Image cleanup complete" + else + log "โญ๏ธ Skipping image removal" + fi + } + + # -------------------------------------------- + # NETWORK CLEANUP + # -------------------------------------------- + cleanup_networks() { + log "๐ŸŒ Cleaning up Docker networks..." + + local HOPS_NETWORKS=("homelab" "traefik" "database") + + for network in "${HOPS_NETWORKS[@]}"; do + if docker network ls --format "{{.Name}}" | grep -q "^${network}$"; then + log "๐Ÿ—‘๏ธ Removing network: $network" + docker network rm "$network" 2>/dev/null || warning "Could not remove network: $network" + fi + done + + log "โœ… Network cleanup complete" + } + + # -------------------------------------------- + # VOLUME CLEANUP + # -------------------------------------------- + cleanup_volumes() { + log "๐Ÿ’พ Cleaning up Docker volumes..." + + local HOPS_VOLUMES=("postgres_data" "redis_data") + + for volume in "${HOPS_VOLUMES[@]}"; do + if docker volume ls --format "{{.Name}}" | grep -q "^${volume}$"; then + log "๐Ÿ—‘๏ธ Removing volume: $volume" + docker volume rm "$volume" 2>/dev/null || warning "Could not remove volume: $volume" + fi + done + + # Clean up orphaned volumes + log "๐Ÿงน Cleaning up orphaned volumes..." + docker volume prune -f 2>/dev/null || true + + log "โœ… Volume cleanup complete" + } + + # -------------------------------------------- + # FILE CLEANUP + # -------------------------------------------- + cleanup_compose_files() { + if [[ $REMOVE_COMPOSE == true && -n "$HOMELAB_DIR" ]]; then + log "๐Ÿ“ Removing Docker Compose files..." + + cd "$HOMELAB_DIR" + + # Backup before removal + if [[ -f docker-compose.yml ]]; then + local BACKUP_FILE="docker-compose.yml.removed.$(date +%Y%m%d%H%M%S)" + log "๐Ÿ“ฆ Backing up compose file to: $BACKUP_FILE" + cp docker-compose.yml "$BACKUP_FILE" 2>/dev/null || warning "Could not backup compose file" + rm -f docker-compose.yml + fi + + # Remove other compose-related files + rm -f docker-compose.override.yml .env 2>/dev/null || true + + # Remove empty homelab directory if it's empty + cd .. + if [[ -d "$HOMELAB_DIR" ]]; then + rmdir "$HOMELAB_DIR" 2>/dev/null && log "๐Ÿ“ Removed empty homelab directory" || log "๐Ÿ“ Homelab directory not empty, keeping it" + fi + + log "โœ… Compose file cleanup complete" + else + log "โญ๏ธ Skipping compose file removal" + fi + } + + cleanup_appdata() { + if [[ $REMOVE_APPDATA == true && -n "$APPDATA_DIR" ]]; then + log "๐Ÿ—‚๏ธ Removing application data..." + + echo -e "\nโš ๏ธ FINAL WARNING: This will delete ALL application configurations!" + echo -e "Application data directory: $APPDATA_DIR" + echo -e "โ“ Are you absolutely sure? Type 'DELETE' to confirm: " + read -r delete_confirm + + if [[ "$delete_confirm" == "DELETE" ]]; then + # Create a backup first + local BACKUP_DIR="/tmp/hops-appdata-backup-$(date +%Y%m%d%H%M%S)" + log "๐Ÿ“ฆ Creating backup at: $BACKUP_DIR" + + if cp -r "$APPDATA_DIR" "$BACKUP_DIR" 2>/dev/null; then + log "โœ… Backup created successfully" + + # Remove the original + if rm -rf "$APPDATA_DIR" 2>/dev/null; then + log "โœ… Application data removed" + log "๐Ÿ“ฆ Backup available at: $BACKUP_DIR" + else + warning "Failed to remove application data directory" + fi + else + warning "Could not create backup, skipping appdata removal" + fi + else + log "๐Ÿšซ Application data removal cancelled" + fi + else + log "โญ๏ธ Skipping application data removal" + fi + } + + # -------------------------------------------- + # FIREWALL CLEANUP + # -------------------------------------------- + cleanup_firewall() { + if [[ $REMOVE_FIREWALL == true ]]; then + log "๐Ÿ”ฅ Removing firewall rules..." + + if command -v ufw &>/dev/null; then + # Remove HOPS-specific rules by searching for comments + local rules_to_remove=() + + # Get numbered rules that contain HOPS service names + mapfile -t rules_to_remove < <(ufw status numbered | grep -E "(sonarr|radarr|lidarr|readarr|bazarr|prowlarr|jellyfin|plex|portainer|traefik|npm)" | awk '{print $1}' | tr -d '[]') + + # Remove rules in reverse order to maintain numbering + if [[ ${#rules_to_remove[@]} -gt 0 ]]; then + for ((i=${#rules_to_remove[@]}-1; i>=0; i--)); do + local rule_num="${rules_to_remove[i]}" + log "๐Ÿ—‘๏ธ Removing firewall rule #$rule_num" + echo "y" | ufw delete "$rule_num" 2>/dev/null || true + done + fi + + log "โœ… Firewall cleanup complete" + else + log "โ„น๏ธ UFW not installed, skipping firewall cleanup" + fi + else + log "โญ๏ธ Skipping firewall cleanup" + fi + } + + # -------------------------------------------- + # DOCKER UNINSTALLATION + # -------------------------------------------- + uninstall_docker() { + if [[ $REMOVE_DOCKER == true ]]; then + log "๐Ÿณ Uninstalling Docker..." + + # Stop Docker service + systemctl stop docker 2>/dev/null || true + systemctl disable docker 2>/dev/null || true + + # Remove Docker packages + local DOCKER_PACKAGES=( + "docker-ce" "docker-ce-cli" "containerd.io" "docker-buildx-plugin" + "docker-compose-plugin" "docker.io" "docker-doc" "docker-compose" + "podman-docker" "containerd" "runc" + ) + + for package in "${DOCKER_PACKAGES[@]}"; do + if dpkg -l | grep -q "^ii.*$package"; then + log "๐Ÿ—‘๏ธ Removing package: $package" + apt-get remove -y "$package" 2>/dev/null || true + fi + done + + # Clean up Docker directories + log "๐Ÿ—‘๏ธ Removing Docker directories..." + rm -rf /var/lib/docker /etc/docker /var/run/docker.sock 2>/dev/null || true + rm -rf ~/.docker 2>/dev/null || true + + # Remove from user directories too + if [[ -n "$SUDO_USER" ]]; then + local user_home=$(eval echo "~$SUDO_USER") + rm -rf "$user_home/.docker" 2>/dev/null || true + fi + + # Remove Docker group + if getent group docker &>/dev/null; then + groupdel docker 2>/dev/null || warning "Could not remove docker group" + fi + + # Clean up package cache + apt-get autoremove -y 2>/dev/null || true + apt-get autoclean 2>/dev/null || true + + log "โœ… Docker uninstallation complete" + else + log "โญ๏ธ Skipping Docker uninstallation" + fi + } + + # -------------------------------------------- + # CLEANUP LOG FILES + # -------------------------------------------- + cleanup_logs() { + log "๐Ÿ“‹ Cleaning up HOPS log files..." + + # Keep current log file, remove others + find "$LOG_DIR" -name "homelab-*.log" -not -name "$(basename "$LOG_FILE")" -delete 2>/dev/null || true + + # Remove log directory if empty (except current log) + local remaining_logs=$(find "$LOG_DIR" -name "*.log" | wc -l) + if [[ $remaining_logs -le 1 ]]; then + log "๐Ÿ“ Log directory will be cleaned after this session" + fi + + log "โœ… Log cleanup complete" + } + + # -------------------------------------------- + # MAIN UNINSTALLATION FLOW + # -------------------------------------------- + show_uninstall_warning + get_uninstall_options + + log "๐Ÿš€ Starting HOPS uninstallation process..." + + find_homelab_directory + detect_running_services + + # Core cleanup steps + stop_and_remove_containers + remove_docker_images + cleanup_networks + cleanup_volumes + cleanup_compose_files + cleanup_appdata + cleanup_firewall + uninstall_docker + cleanup_logs + + # -------------------------------------------- + # FINAL SUMMARY + # -------------------------------------------- + echo -e "\nโœ… HOPS Uninstallation Complete!" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + + log "๐Ÿ“‹ Uninstallation Summary:" + echo -e "\n๐Ÿ—‘๏ธ Removed Components:" + echo " โ€ข Docker containers: โœ…" + echo " โ€ข Docker Compose files: โœ…" + [[ $REMOVE_IMAGES == true ]] && echo " โ€ข Docker images: โœ…" || echo " โ€ข Docker images: โŒ (kept)" + [[ $REMOVE_APPDATA == true ]] && echo " โ€ข Application data: โœ…" || echo " โ€ข Application data: โŒ (kept)" + [[ $REMOVE_DOCKER == true ]] && echo " โ€ข Docker installation: โœ…" || echo " โ€ข Docker installation: โŒ (kept)" + [[ $REMOVE_FIREWALL == true ]] && echo " โ€ข Firewall rules: โœ…" || echo " โ€ข Firewall rules: โŒ (kept)" + + echo -e "\n๐Ÿ“‚ Preserved:" + echo " โ€ข Media files: โœ… (never touched)" + [[ $REMOVE_APPDATA != true ]] && echo " โ€ข Application configurations: โœ…" + + if [[ $REMOVE_APPDATA == true ]]; then + echo -e "\n๐Ÿ“ฆ Backup Location:" + echo " โ€ข Application data backup: /tmp/hops-appdata-backup-*" + echo " โ€ข Consider moving this backup to a permanent location" + fi + + echo -e "\n๐Ÿ“‹ Complete log: $LOG_FILE" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + + log "โœ… HOPS uninstallation completed successfully!" + + if [[ -n "$SUDO_USER" ]]; then + echo -e "\n๐Ÿ’ก Note: You may want to restart your session to ensure all group changes take effect." + fi + + if [[ $REMOVE_DOCKER == true ]]; then + echo -e "\n๐Ÿ”„ Recommendation: Reboot your system to complete Docker removal." + fi + + return 0 +} \ No newline at end of file diff --git a/hops.sh b/hops.sh new file mode 100755 index 0000000..64a66af --- /dev/null +++ b/hops.sh @@ -0,0 +1,679 @@ +#!/bin/bash + +# HOPS - Homelab Orchestration Provisioning Script +# Primary Management Script +# Version: 3.1.0 + +# Exit on any error +set -e + +# Script version and metadata +readonly SCRIPT_VERSION="3.1.0" +readonly SCRIPT_NAME="HOPS" +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Default script locations +readonly INSTALLER_SCRIPT="$SCRIPT_DIR/hops_installer_enhanced.sh" +readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/hops_uninstaller_fixed.sh" +readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/hops_service_definitions.sh" + +# Color codes for output +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly BLUE='\033[0;34m' +readonly PURPLE='\033[0;35m' +readonly CYAN='\033[0;36m' +readonly WHITE='\033[1;37m' +readonly NC='\033[0m' # No Color + +# Logging setup +readonly LOG_DIR="/var/log/hops" +readonly LOG_FILE="$LOG_DIR/hops-main-$(date +%Y%m%d-%H%M%S).log" + +# Initialize logging +init_logging() { + if [[ $EUID -eq 0 ]]; then + mkdir -p "$LOG_DIR" + touch "$LOG_FILE" + fi +} + +# Logging function +log() { + local message="$1" + local timestamp="$(date '+%Y-%m-%d %T')" + + if [[ -w "$LOG_FILE" ]]; then + echo "$timestamp - $message" >> "$LOG_FILE" + fi + + echo -e "$message" +} + +# Error handling +error_exit() { + log "${RED}โŒ ERROR: $1${NC}" + exit 1 +} + +# Warning function +warning() { + log "${YELLOW}โš ๏ธ WARNING: $1${NC}" +} + +# Success function +success() { + log "${GREEN}โœ… $1${NC}" +} + +# Info function +info() { + log "${BLUE}โ„น๏ธ $1${NC}" +} + +# Clear screen and show header +show_header() { + clear + cat << "EOF" + + _ _ ____ ____ ____ + | | | || _ \| _ \/ ___| + | |__| || |_) | |_) \___ \ + | __ || __/| __/ ___) | + |_| |_||_| |_| |____/ + +EOF + echo -e "${CYAN}๐Ÿš€ Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}${NC}" + echo -e "${WHITE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}\n" +} + +# Check if running as root +check_root() { + if [[ $EUID -ne 0 ]]; then + error_exit "This script must be run as root or with sudo." + fi +} + +# Validate script dependencies +check_dependencies() { + local missing_deps=() + + # Check for required scripts + if [[ ! -f "$INSTALLER_SCRIPT" ]]; then + missing_deps+=("Installer script: $INSTALLER_SCRIPT") + fi + + if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then + missing_deps+=("Uninstaller script: $UNINSTALLER_SCRIPT") + fi + + if [[ ! -f "$SERVICE_DEFINITIONS" ]]; then + missing_deps+=("Service definitions: $SERVICE_DEFINITIONS") + fi + + # Check for required commands + local required_commands=("curl" "wget" "systemctl") + for cmd in "${required_commands[@]}"; do + if ! command -v "$cmd" &>/dev/null; then + missing_deps+=("Command: $cmd") + fi + done + + if [[ ${#missing_deps[@]} -gt 0 ]]; then + error_exit "Missing dependencies:\n$(printf ' โ€ข %s\n' "${missing_deps[@]}")" + fi +} + +# Check system requirements +check_system_requirements() { + info "Checking system requirements..." + + # Check OS + if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then + warning "This script is designed for Ubuntu/Debian/Mint systems" + echo -e "Continue anyway? [y/N]: " + read -r continue_choice + [[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 0 + fi + + # Check minimum requirements + local ram_gb=$(free -g | awk '/^Mem:/{print $2}') + local disk_gb=$(df -BG --output=avail / | tail -n 1 | tr -d 'G') + + if [[ $ram_gb -lt 2 ]]; then + warning "Low RAM detected: ${ram_gb}GB (2GB+ recommended)" + fi + + if [[ $disk_gb -lt 10 ]]; then + warning "Low disk space: ${disk_gb}GB (10GB+ recommended)" + fi + + success "System requirements check complete" +} + +# Get HOPS installation status +get_installation_status() { + local status="not_installed" + local homelab_dirs=( + "$HOME/homelab" + "/home/*/homelab" + "/opt/homelab" + "/srv/homelab" + ) + + # Check for existing installation + for dir in "${homelab_dirs[@]}"; do + if [[ -f "$dir/docker-compose.yml" ]]; then + status="installed" + HOMELAB_DIR="$dir" + break + fi + done + + # Check for running containers + if command -v docker &>/dev/null && docker ps --format "{{.Names}}" | grep -qE "(sonarr|radarr|jellyfin|plex|portainer)"; then + if [[ "$status" == "not_installed" ]]; then + status="partial" + else + status="running" + fi + fi + + echo "$status" +} + +# Show installation status +show_status() { + local status=$(get_installation_status) + + echo -e "${WHITE}๐Ÿ“Š Current Status:${NC}" + case "$status" in + "not_installed") + echo -e " ${RED}โ— Not Installed${NC}" + ;; + "installed") + echo -e " ${YELLOW}โ— Installed (stopped)${NC}" + echo -e " ${BLUE}๐Ÿ“‚ Location: $HOMELAB_DIR${NC}" + ;; + "running") + echo -e " ${GREEN}โ— Running${NC}" + echo -e " ${BLUE}๐Ÿ“‚ Location: $HOMELAB_DIR${NC}" + ;; + "partial") + echo -e " ${YELLOW}โ— Partial Installation${NC}" + ;; + esac + echo +} + +# Run installer +run_installer() { + info "Launching HOPS installer..." + + if [[ ! -f "$INSTALLER_SCRIPT" ]]; then + error_exit "Installer script not found: $INSTALLER_SCRIPT" + fi + + # Source the installer function and run it + if source "$INSTALLER_SCRIPT" && install_hops; then + success "Installation completed successfully!" + else + error_exit "Installation failed. Check logs for details." + fi + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Run uninstaller +run_uninstaller() { + info "Launching HOPS uninstaller..." + + if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then + error_exit "Uninstaller script not found: $UNINSTALLER_SCRIPT" + fi + + # Source the uninstaller function and run it + if source "$UNINSTALLER_SCRIPT" && uninstall_hops; then + success "Uninstallation completed successfully!" + else + error_exit "Uninstallation failed. Check logs for details." + fi + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Show service status +show_service_status() { + show_header + echo -e "${WHITE}๐Ÿ” Service Status Check${NC}\n" + + if ! command -v docker &>/dev/null; then + warning "Docker is not installed" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + local status=$(get_installation_status) + if [[ "$status" == "not_installed" ]]; then + warning "HOPS is not installed" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + echo -e "${BLUE}๐Ÿ“Š Docker Service Status:${NC}" + if systemctl is-active --quiet docker; then + echo -e " ${GREEN}โ— Docker daemon: Running${NC}" + else + echo -e " ${RED}โ— Docker daemon: Stopped${NC}" + fi + + echo -e "\n${BLUE}๐Ÿ“ฆ Container Status:${NC}" + + # Source service definitions to get port info + if [[ -f "$SERVICE_DEFINITIONS" ]]; then + source "$SERVICE_DEFINITIONS" + fi + + # Known HOPS services with their ports + local services=( + "sonarr:8989" "radarr:7878" "lidarr:8686" "readarr:8787" + "bazarr:6767" "prowlarr:9696" "jellyfin:8096" "plex:32400" + "overseerr:5055" "jellyseerr:5056" "portainer:9000" + "traefik:8080" "nginx-proxy-manager:81" "qbittorrent:8082" + "transmission:9091" "nzbget:6789" "sabnzbd:8080" + "uptime-kuma:3001" "jellystat:3000" + ) + + local running_count=0 + local total_count=0 + + for service_info in "${services[@]}"; do + local service_name="${service_info%:*}" + local service_port="${service_info#*:}" + + if docker ps --format "{{.Names}}" | grep -q "^${service_name}$"; then + ((total_count++)) + local status_symbol="${GREEN}โ—${NC}" + local status_text="Running" + + # Check if port is accessible + if curl -sSf --max-time 2 --connect-timeout 1 "http://localhost:${service_port}" >/dev/null 2>&1; then + status_text="Running & Accessible" + ((running_count++)) + else + status_text="Running (starting up)" + status_symbol="${YELLOW}โ—${NC}" + fi + + printf " %s %-20s %s (:%s)\n" "$status_symbol" "$service_name" "$status_text" "$service_port" + elif docker ps -a --format "{{.Names}}" | grep -q "^${service_name}$"; then + ((total_count++)) + printf " %s %-20s %s\n" "${RED}โ—${NC}" "$service_name" "Stopped" + fi + done + + if [[ $total_count -eq 0 ]]; then + echo -e " ${YELLOW}No HOPS services found${NC}" + else + echo -e "\n${WHITE}๐Ÿ“ˆ Summary: ${running_count}/${total_count} services running and accessible${NC}" + fi + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Manage services (start/stop/restart) +manage_services() { + show_header + echo -e "${WHITE}๐ŸŽ›๏ธ Service Management${NC}\n" + + local status=$(get_installation_status) + if [[ "$status" == "not_installed" ]]; then + warning "HOPS is not installed" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + if [[ -z "$HOMELAB_DIR" ]]; then + error_exit "Cannot locate homelab directory with docker-compose.yml" + fi + + echo -e "${BLUE}Available actions:${NC}" + echo -e " 1) Start all services" + echo -e " 2) Stop all services" + echo -e " 3) Restart all services" + echo -e " 4) View logs (recent)" + echo -e " 5) View logs (follow)" + echo -e " 6) Update services" + echo -e " 7) Restart individual service" + echo -e " 8) Back to main menu" + + echo -e "\n${WHITE}Select an option [1-8]: ${NC}" + read -r choice + + cd "$HOMELAB_DIR" + + case $choice in + 1) + info "Starting all services..." + if docker compose up -d; then + success "Services started" + else + warning "Some services may have failed to start" + fi + ;; + 2) + info "Stopping all services..." + if docker compose down; then + success "Services stopped" + else + warning "Some services may not have stopped cleanly" + fi + ;; + 3) + info "Restarting all services..." + if docker compose restart; then + success "Services restarted" + else + warning "Some services may have failed to restart" + fi + ;; + 4) + info "Showing recent logs..." + docker compose logs --tail=100 + ;; + 5) + info "Following logs (Ctrl+C to exit)..." + docker compose logs -f --tail=50 + ;; + 6) + info "Updating services..." + if docker compose pull && docker compose up -d; then + success "Services updated" + else + warning "Update may have failed" + fi + ;; + 7) + echo -e "\n${WHITE}Available services:${NC}" + docker compose ps --services | nl -w2 -s') ' + echo -e "\n${WHITE}Enter service name to restart: ${NC}" + read -r service_name + if [[ -n "$service_name" ]]; then + info "Restarting $service_name..." + if docker compose restart "$service_name"; then + success "$service_name restarted" + else + warning "Failed to restart $service_name" + fi + fi + ;; + 8) + return + ;; + *) + warning "Invalid option" + ;; + esac + + echo -e "\n${WHITE}Press Enter to continue...${NC}" + read -r +} + +# Show quick access URLs +show_access_info() { + show_header + echo -e "${WHITE}๐ŸŒ Service Access Information${NC}\n" + + local status=$(get_installation_status) + if [[ "$status" == "not_installed" ]]; then + warning "HOPS is not installed" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + echo -e "${BLUE}๐Ÿ“ฑ Access your services at:${NC}" + + # Get local IP + local local_ip=$(hostname -I | awk '{print $1}') + + # Service URLs with paths + local services=( + "Sonarr:8989:/sonarr" + "Radarr:7878:/radarr" + "Lidarr:8686:/lidarr" + "Readarr:8787:/readarr" + "Bazarr:6767:" + "Prowlarr:9696:" + "Jellyfin:8096:" + "Plex:32400:/web" + "Overseerr:5055:" + "Jellyseerr:5056:" + "Portainer:9000:" + "Traefik:8080:" + "NPM:81:" + "qBittorrent:8082:" + "Transmission:9091:" + "NZBGet:6789:" + "SABnzbd:8080:" + "Uptime-Kuma:3001:" + "Jellystat:3000:" + ) + + local active_services=0 + for service_info in "${services[@]}"; do + local service_name="${service_info%%:*}" + local service_port="${service_info#*:}" + local service_path="${service_port#*:}" + service_port="${service_port%:*}" + + if docker ps --format "{{.Names}}" | grep -qi "${service_name,,}"; then + local url="http://${local_ip}:${service_port}${service_path}" + printf " ${GREEN}โ—${NC} %-15s %s\n" "$service_name" "$url" + ((active_services++)) + fi + done + + if [[ $active_services -eq 0 ]]; then + echo -e " ${YELLOW}No services currently running${NC}" + fi + + echo -e "\n${YELLOW}๐Ÿ’ก Tips:${NC}" + echo -e " โ€ข Bookmark these URLs for easy access" + echo -e " โ€ข Default credentials are in the .env file" + echo -e " โ€ข Change default passwords after first login" + echo -e " โ€ข Some services may take a few minutes to fully start" + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Show logs +show_logs() { + show_header + echo -e "${WHITE}๐Ÿ“‹ HOPS Logs${NC}\n" + + if [[ ! -d "$LOG_DIR" ]]; then + warning "No log directory found" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + echo -e "${BLUE}Available log files:${NC}" + local log_files=($(find "$LOG_DIR" -name "*.log" -type f | sort -r)) + + if [[ ${#log_files[@]} -eq 0 ]]; then + warning "No log files found" + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r + return + fi + + local count=1 + for log_file in "${log_files[@]}"; do + local basename_log=$(basename "$log_file") + local size=$(du -h "$log_file" | cut -f1) + local date=$(stat -c %y "$log_file" | cut -d' ' -f1) + + printf " %d) %-40s (%s, %s)\n" "$count" "$basename_log" "$size" "$date" + ((count++)) + done + + echo -e "\n${WHITE}Select a log file to view [1-${#log_files[@]}] or 0 to go back: ${NC}" + read -r choice + + if [[ "$choice" -eq 0 ]]; then + return + elif [[ "$choice" -gt 0 && "$choice" -le ${#log_files[@]} ]]; then + local selected_log="${log_files[$((choice-1))]}" + echo -e "\n${BLUE}Showing last 50 lines of $(basename "$selected_log"):${NC}\n" + tail -50 "$selected_log" + else + warning "Invalid selection" + fi + + echo -e "\n${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Show help information +show_help() { + show_header + echo -e "${WHITE}๐Ÿ“š HOPS Help & Documentation${NC}\n" + + echo -e "${BLUE}๐ŸŽฏ What is HOPS?${NC}" + echo -e "HOPS (Homelab Orchestration Provisioning Script) is an automated installer" + echo -e "for popular homelab applications including media servers, download clients," + echo -e "and monitoring tools.\n" + + echo -e "${BLUE}๐Ÿš€ Quick Start:${NC}" + echo -e " 1. Run this script as root/sudo" + echo -e " 2. Choose 'Install HOPS' from the menu" + echo -e " 3. Configure directories and timezone" + echo -e " 4. Select your desired services" + echo -e " 5. Wait for installation to complete" + echo -e " 6. Access services via the provided URLs\n" + + echo -e "${BLUE}๐Ÿ“ฑ Supported Services:${NC}" + echo -e " โ€ข Media Management: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr" + echo -e " โ€ข Download Clients: qBittorrent, Transmission, NZBGet, SABnzbd" + echo -e " โ€ข Media Servers: Jellyfin, Plex, Emby, Jellystat" + echo -e " โ€ข Request Management: Overseerr, Jellyseerr, Ombi" + echo -e " โ€ข Reverse Proxy: Traefik, Nginx Proxy Manager" + echo -e " โ€ข Monitoring: Portainer, Uptime Kuma, Watchtower\n" + + echo -e "${BLUE}๐Ÿ”ง Requirements:${NC}" + echo -e " โ€ข Ubuntu/Debian/Mint Linux" + echo -e " โ€ข 2GB+ RAM (4GB+ recommended)" + echo -e " โ€ข 10GB+ free disk space" + echo -e " โ€ข Root/sudo access" + echo -e " โ€ข Internet connection\n" + + echo -e "${BLUE}๐Ÿ“ Default Locations:${NC}" + echo -e " โ€ข Homelab directory: ~/homelab/" + echo -e " โ€ข App configurations: /opt/appdata/" + echo -e " โ€ข Media storage: /mnt/media/" + echo -e " โ€ข Logs: /var/log/hops/\n" + + echo -e "${BLUE}๐Ÿ†˜ Troubleshooting:${NC}" + echo -e " โ€ข Check logs in the 'View Logs' menu" + echo -e " โ€ข Verify Docker is running: systemctl status docker" + echo -e " โ€ข Check container status: docker ps" + echo -e " โ€ข View service logs: docker logs [service-name]" + echo -e " โ€ข Restart services: docker compose restart [service-name]\n" + + echo -e "${BLUE}๐Ÿ” Security Notes:${NC}" + echo -e " โ€ข Change default passwords in .env file after installation" + echo -e " โ€ข Configure firewall rules as needed" + echo -e " โ€ข Regularly update services using the management menu\n" + + echo -e "${WHITE}Press Enter to return to main menu...${NC}" + read -r +} + +# Main menu +show_main_menu() { + local status=$(get_installation_status) + + echo -e "${WHITE}๐ŸŽ›๏ธ Main Menu:${NC}" + echo -e " 1) Install HOPS" + + if [[ "$status" != "not_installed" ]]; then + echo -e " 2) Uninstall HOPS" + echo -e " 3) Manage Services" + echo -e " 4) Service Status" + echo -e " 5) Access Information" + else + echo -e " 2) Uninstall HOPS ${YELLOW}(not installed)${NC}" + echo -e " 3) Manage Services ${YELLOW}(not installed)${NC}" + echo -e " 4) Service Status ${YELLOW}(not installed)${NC}" + echo -e " 5) Access Information ${YELLOW}(not installed)${NC}" + fi + + echo -e " 6) View Logs" + echo -e " 7) Help & Documentation" + echo -e " 8) Exit" + + echo -e "\n${WHITE}Select an option [1-8]: ${NC}" +} + +# Main program loop +main() { + init_logging + check_root + check_dependencies + + while true; do + show_header + show_status + show_main_menu + + read -r choice + + case $choice in + 1) + check_system_requirements + run_installer + ;; + 2) + run_uninstaller + ;; + 3) + manage_services + ;; + 4) + show_service_status + ;; + 5) + show_access_info + ;; + 6) + show_logs + ;; + 7) + show_help + ;; + 8) + info "Thank you for using HOPS!" + exit 0 + ;; + *) + warning "Invalid option. Please select 1-8." + sleep 2 + ;; + esac + done +} + +# Script entry point +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/hops_installer_enhanced.sh b/hops_installer_enhanced.sh new file mode 100755 index 0000000..0bed53c --- /dev/null +++ b/hops_installer_enhanced.sh @@ -0,0 +1,924 @@ +#!/bin/bash + +install_hops() { + # Clear terminal at startup + clear + + # Exit on any error + set -e + + # Script version for update tracking + local SCRIPT_VERSION="3.1.0" + local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + # -------------------------------------------- + # LOGGING SETUP + # -------------------------------------------- + local LOG_DIR="/var/log/hops" + local LOG_FILE="$LOG_DIR/homelab-setup-$(date +%Y%m%d-%H%M%S).log" + mkdir -p "$LOG_DIR" + touch "$LOG_FILE" + + log() { + echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE" + } + + error_exit() { + log "โŒ ERROR: $1" + log "โŒ Installation failed. Check logs at: $LOG_FILE" + exit 1 + } + + # Enhanced error handling with rollback + DEPLOYMENT_STEPS_COMPLETED=() + + track_step() { + DEPLOYMENT_STEPS_COMPLETED+=("$1") + log "โœ… Step completed: $1" + } + + rollback_deployment() { + log "๐Ÿ”„ Rolling back deployment..." + + for step in "${DEPLOYMENT_STEPS_COMPLETED[@]}"; do + case "$step" in + "containers_started") + log "๐Ÿ›‘ Stopping containers..." + docker compose down --timeout 30 2>/dev/null || true + ;; + "images_pulled") + log "๐Ÿ—‘๏ธ Removing pulled images..." + docker compose down --rmi all 2>/dev/null || true + ;; + "directories_created") + log "๐Ÿ“ Cleaning up directories..." + [[ -n "$APPDATA_DIR" ]] && rm -rf "$APPDATA_DIR" 2>/dev/null || true + ;; + "compose_generated") + log "๐Ÿ“ Removing compose file..." + [[ -f "docker-compose.yml" ]] && rm -f docker-compose.yml + ;; + esac + done + + log "๐Ÿ”„ Rollback completed" + } + + error_exit_with_rollback() { + log "โŒ ERROR: $1" + rollback_deployment + log "โŒ Installation failed and rolled back. Check logs at: $LOG_FILE" + exit 1 + } + + # -------------------------------------------- + # HEADER + # -------------------------------------------- + cat << "EOF" + + _ _ ____ ____ ____ + | | | || _ \| _ \/ ___| + | |__| || |_) | |_) \___ \ + | __ || __/| __/ ___) | + |_| |_||_| |_| |____/ + +EOF + echo -e "๐Ÿš€ Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}\n" + log "๐Ÿš€ Starting HOPS Deployment v${SCRIPT_VERSION}" + + # -------------------------------------------- + # SYSTEM REQUIREMENTS CHECK + # -------------------------------------------- + check_system_requirements() { + local MIN_RAM_GB=2 + local MIN_DISK_GB=10 + local MIN_CORES=2 + + log "๐Ÿ” Checking system requirements..." + + # Check RAM + local RAM_GB=$(free -g | awk '/^Mem:/{print $2}') + if [[ $RAM_GB -lt $MIN_RAM_GB ]]; then + error_exit "Insufficient RAM: ${RAM_GB}GB detected, ${MIN_RAM_GB}GB required" + fi + + # Check disk space + local DISK_AVAIL=$(df -BG --output=avail / | tail -n 1 | tr -d 'G') + if [[ $DISK_AVAIL -lt $MIN_DISK_GB ]]; then + error_exit "Insufficient disk space: ${DISK_AVAIL}GB available, ${MIN_DISK_GB}GB required" + fi + + # Check CPU cores + local CPU_CORES=$(nproc) + if [[ $CPU_CORES -lt $MIN_CORES ]]; then + log "โš ๏ธ Low CPU cores: ${CPU_CORES} detected, ${MIN_CORES} recommended" + fi + + log "โœ… System meets minimum requirements (${RAM_GB}GB RAM, ${CPU_CORES} cores, ${DISK_AVAIL}GB disk)" + } + + # -------------------------------------------- + # REQUIRED PACKAGES CHECK + # -------------------------------------------- + check_required_packages() { + local missing_packages=() + local required_packages=("curl" "wget" "openssl" "lsof" "apache2-utils") + + log "๐Ÿ“ฆ Checking required packages..." + + for package in "${required_packages[@]}"; do + if ! command -v "${package%%-*}" &>/dev/null; then + missing_packages+=("$package") + fi + done + + if [[ ${#missing_packages[@]} -gt 0 ]]; then + log "๐Ÿ“ฆ Installing missing packages: ${missing_packages[*]}" + apt-get update && apt-get install -y "${missing_packages[@]}" + fi + + log "โœ… All required packages are installed" + } + + # -------------------------------------------- + # ROOT CHECK + # -------------------------------------------- + if [[ $EUID -ne 0 ]]; then + error_exit "This script must be run as root or with sudo." + fi + + # -------------------------------------------- + # OS DETECTION + # -------------------------------------------- + detect_os() { + if command -v lsb_release &>/dev/null; then + OS_NAME=$(lsb_release -is) + OS_VERSION=$(lsb_release -rs) + else + OS_NAME=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"') + OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release | cut -d= -f2 | tr -d '"') + fi + OS_NAME_LOWER=$(echo "$OS_NAME" | tr '[:upper:]' '[:lower:]') + + if [[ ! "$OS_NAME_LOWER" =~ ^(ubuntu|debian|linuxmint|mint)$ ]]; then + error_exit "Unsupported OS: $OS_NAME Only Debian/Ubuntu/Mint supported" + fi + log "โœ… Detected OS: $OS_NAME $OS_VERSION" + } + + # -------------------------------------------- + # USER CONFIGURATION COLLECTION + # -------------------------------------------- + collect_user_configuration() { + log "๐Ÿ”ง Collecting user configuration..." + + # Get running user info + if [[ -n "$SUDO_USER" ]]; then + RUNNING_USER="$SUDO_USER" + PUID=$(id -u "$SUDO_USER") + PGID=$(id -g "$SUDO_USER") + else + RUNNING_USER="root" + PUID=1000 + PGID=1000 + log "โš ๏ธ Running as root, defaulting to PUID=1000, PGID=1000" + fi + + # Timezone configuration + echo -e "\n๐ŸŒ Timezone Configuration" + echo "Current timezone: $(timedatectl show --property=Timezone --value 2>/dev/null || echo "Unknown")" + echo -e "Keep current timezone? [Y/n]: " + read -r keep_tz + + if [[ "$keep_tz" =~ ^[Nn]$ ]]; then + echo -e "Enter timezone (e.g., America/New_York, Europe/London): " + read -r user_timezone + validate_timezone "$user_timezone" + TIMEZONE="$user_timezone" + else + TIMEZONE=$(timedatectl show --property=Timezone --value 2>/dev/null || echo "America/New_York") + fi + + # Directory configuration + echo -e "\n๐Ÿ“ Directory Configuration" + echo -e "Media directory [/mnt/media]: " + read -r media_dir + MEDIA_DIR="${media_dir:-/mnt/media}" + + echo -e "Application data directory [/opt/appdata]: " + read -r appdata_dir + APPDATA_DIR="${appdata_dir:-/opt/appdata}" + + # Create directories + mkdir -p "$MEDIA_DIR"/{movies,tv,music,books,downloads} + mkdir -p "$APPDATA_DIR" + + # Set ownership if not root + if [[ "$RUNNING_USER" != "root" ]]; then + chown -R "$PUID:$PGID" "$MEDIA_DIR" "$APPDATA_DIR" 2>/dev/null || true + fi + + log "โœ… User configuration collected" + log " User: $RUNNING_USER ($PUID:$PGID)" + log " Timezone: $TIMEZONE" + log " Media: $MEDIA_DIR" + log " AppData: $APPDATA_DIR" + } + + # -------------------------------------------- + # VALIDATION FUNCTIONS + # -------------------------------------------- + validate_timezone() { + if ! timedatectl list-timezones | grep -qx "$1" 2>/dev/null; then + log "โš ๏ธ Timezone '$1' invalid, defaulting to 'America/New_York'" + TIMEZONE="America/New_York" + fi + } + + validate_password() { + local password="$1" + local min_length="${2:-12}" + + if [[ -z "$password" ]]; then + echo -e "\n๐Ÿ” Password must meet these requirements:" + echo " โ€ข Minimum $min_length characters" + echo " โ€ข At least one uppercase letter" + echo " โ€ข At least one lowercase letter" + echo " โ€ข At least one number" + echo " โ€ข At least one special character" + return 3 + fi + + if [[ ${#password} -lt $min_length ]]; then + return 1 + fi + + if [[ ! "$password" =~ [A-Z] ]] || [[ ! "$password" =~ [a-z] ]] || \ + [[ ! "$password" =~ [0-9] ]] || [[ ! "$password" =~ [^A-Za-z0-9] ]]; then + return 2 + fi + + return 0 + } + + check_port() { + local PORT=$1 + local SERVICE=$2 + if lsof -i :"$PORT" >/dev/null 2>&1; then + local PROCESS=$(lsof -ti :"$PORT" | head -1) + local PROCESS_NAME=$(ps -p "$PROCESS" -o comm= 2>/dev/null || echo "unknown") + log "โš ๏ธ Port $PORT is already in use by $PROCESS_NAME. $SERVICE may fail to start." + return 1 + fi + return 0 + } + + check_all_ports() { + local SERVICES=("$@") + local CONFLICTS=() + + # Source service definitions to get port mappings + if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then + source "$SCRIPT_DIR/hops_service_definitions.sh" + fi + + for svc in "${SERVICES[@]}"; do + local ports=$(get_service_ports "$svc") + for port in $ports; do + if ! check_port "$port" "$svc"; then + CONFLICTS+=("Port $port ($svc)") + fi + done + done + + if [[ ${#CONFLICTS[@]} -gt 0 ]]; then + log "โš ๏ธ Found ${#CONFLICTS[@]} port conflicts:" + for conflict in "${CONFLICTS[@]}"; do + log " โ€ข $conflict" + done + + echo -e "\nโš ๏ธ Port conflicts detected! Continue anyway? (y/N): " + read -r continue_choice + if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then + error_exit "Installation cancelled due to port conflicts." + fi + return 1 + fi + return 0 + } + + # -------------------------------------------- + # DOCKER COMPOSE VERSION CHECK + # -------------------------------------------- + check_docker_compose_version() { + # Check for Docker Compose plugin (v2) + if docker compose version &>/dev/null; then + log "โœ… Docker Compose plugin detected ($(docker compose version --short))" + return 0 + fi + + # Check for standalone docker-compose (v1) + if command -v docker-compose &>/dev/null; then + log "โš ๏ธ Found legacy docker-compose (v1). Installing Docker Compose plugin..." + if ! apt-get install -y docker-compose-plugin 2>&1 | tee -a "$LOG_FILE"; then + error_exit "Failed to install Docker Compose plugin." + fi + return 0 + fi + + # Neither found + error_exit "No Docker Compose detected. Please install Docker first." + } + + # -------------------------------------------- + # IMPROVED PASSWORD GENERATION + # -------------------------------------------- + generate_secure_password() { + local length="${1:-16}" + local max_attempts=5 + local attempt=1 + + while [[ $attempt -le $max_attempts ]]; do + # Generate password with mixed case, numbers, and symbols + local password=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-${length}) + + # Ensure it meets complexity requirements + if validate_password "$password" "$length"; then + echo "$password" + return 0 + fi + + ((attempt++)) + done + + # Fallback: construct a guaranteed compliant password + local upper=$(tr -dc 'A-Z' < /dev/urandom | head -c2) + local lower=$(tr -dc 'a-z' < /dev/urandom | head -c4) + local digits=$(tr -dc '0-9' < /dev/urandom | head -c2) + local symbols=$(tr -dc '!@#$%^&*' < /dev/urandom | head -c2) + local remaining_length=$((length - 10)) + + if [[ $remaining_length -gt 0 ]]; then + local remaining=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c$remaining_length) + echo "${upper}${lower}${digits}${symbols}${remaining}" | fold -w1 | shuf | tr -d '\n' + else + echo "${upper}${lower}${digits}${symbols}" + fi + } + + # -------------------------------------------- + # ENVIRONMENT FILE GENERATION + # -------------------------------------------- + create_env_file() { + local homelab_dir="$1" + + log "๐Ÿ“ Creating environment file..." + + cat > "$homelab_dir/.env" </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/hops_service_definitions.sh b/hops_service_definitions.sh new file mode 100755 index 0000000..f4da945 --- /dev/null +++ b/hops_service_definitions.sh @@ -0,0 +1,1285 @@ +#!/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/hops_uninstaller_fixed.sh b/hops_uninstaller_fixed.sh new file mode 100755 index 0000000..709678c --- /dev/null +++ b/hops_uninstaller_fixed.sh @@ -0,0 +1,560 @@ +#!/bin/bash + +uninstall_hops() { + # Clear terminal at startup + clear + + # Exit on any error (but allow some failures during cleanup) + set +e + + # Script version for consistency + local SCRIPT_VERSION="3.1.0" + + # -------------------------------------------- + # LOGGING SETUP + # -------------------------------------------- + local LOG_DIR="/var/log/hops" + local LOG_FILE="$LOG_DIR/homelab-uninstall-$(date +%Y%m%d-%H%M%S).log" + mkdir -p "$LOG_DIR" + touch "$LOG_FILE" + + log() { + echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE" + } + + error_exit() { + log "โŒ ERROR: $1" + log "โŒ Uninstallation failed. Check logs at: $LOG_FILE" + exit 1 + } + + warning() { + log "โš ๏ธ WARNING: $1" + } + + # -------------------------------------------- + # HEADER + # -------------------------------------------- + cat << "EOF" + + _ _ ____ ____ ____ + | | | || _ \| _ \/ ___| + | |__| || |_) | |_) \___ \ + | __ || __/| __/ ___) | + |_| |_||_| |_| |____/ + +EOF + echo -e "๐Ÿ—‘๏ธ Homelab Orchestration Provisioning Script - UNINSTALLER v${SCRIPT_VERSION}\n" + log "๐Ÿ—‘๏ธ Starting HOPS Uninstallation v${SCRIPT_VERSION}" + + # -------------------------------------------- + # ROOT CHECK + # -------------------------------------------- + if [[ $EUID -ne 0 ]]; then + error_exit "This script must be run as root or with sudo." + fi + + # -------------------------------------------- + # CONFIRMATION PROMPT + # -------------------------------------------- + show_uninstall_warning() { + echo -e "โš ๏ธ WARNING: This will completely remove your HOPS installation!" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo -e "This uninstaller will:" + echo -e " โ€ข Stop and remove all Docker containers" + echo -e " โ€ข Remove Docker images (optional)" + echo -e " โ€ข Remove Docker Compose configuration" + echo -e " โ€ข Clean up application data (optional)" + echo -e " โ€ข Remove firewall rules" + echo -e " โ€ข Uninstall Docker (optional)" + echo -e "" + echo -e "โš ๏ธ YOUR MEDIA FILES WILL NOT BE DELETED" + echo -e "โš ๏ธ APPLICATION DATA REMOVAL IS OPTIONAL" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + } + + get_uninstall_options() { + echo -e "\n๐Ÿ”ง Uninstall Options:" + + # Container and compose removal (always done) + REMOVE_CONTAINERS=true + REMOVE_COMPOSE=true + + # Optional removals + echo -e "\nโ“ Remove Docker images? (saves disk space but requires re-download) [y/N]: " + read -r remove_images + REMOVE_IMAGES=false + [[ "$remove_images" =~ ^[Yy]$ ]] && REMOVE_IMAGES=true + + echo -e "\nโ“ Remove application data? (โš ๏ธ DELETES ALL CONFIGURATIONS!) [y/N]: " + read -r remove_appdata + REMOVE_APPDATA=false + [[ "$remove_appdata" =~ ^[Yy]$ ]] && REMOVE_APPDATA=true + + echo -e "\nโ“ Uninstall Docker completely? [y/N]: " + read -r remove_docker + REMOVE_DOCKER=false + [[ "$remove_docker" =~ ^[Yy]$ ]] && REMOVE_DOCKER=true + + echo -e "\nโ“ Remove firewall rules? [Y/n]: " + read -r remove_firewall + REMOVE_FIREWALL=true + [[ "$remove_firewall" =~ ^[Nn]$ ]] && REMOVE_FIREWALL=false + + # Final confirmation + echo -e "\nโš ๏ธ FINAL CONFIRMATION" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + echo -e "Actions to perform:" + echo -e " โ€ข Remove containers: โœ…" + echo -e " โ€ข Remove compose files: โœ…" + [[ $REMOVE_IMAGES == true ]] && echo -e " โ€ข Remove Docker images: โœ…" || echo -e " โ€ข Remove Docker images: โŒ" + [[ $REMOVE_APPDATA == true ]] && echo -e " โ€ข Remove app data: โœ…" || echo -e " โ€ข Remove app data: โŒ" + [[ $REMOVE_DOCKER == true ]] && echo -e " โ€ข Uninstall Docker: โœ…" || echo -e " โ€ข Uninstall Docker: โŒ" + [[ $REMOVE_FIREWALL == true ]] && echo -e " โ€ข Remove firewall rules: โœ…" || echo -e " โ€ข Remove firewall rules: โŒ" + + echo -e "\nโ“ Proceed with uninstallation? [y/N]: " + read -r final_confirm + if [[ ! "$final_confirm" =~ ^[Yy]$ ]]; then + log "๐Ÿšซ Uninstallation cancelled by user" + exit 0 + fi + } + + # -------------------------------------------- + # HOMELAB DIRECTORY DETECTION + # -------------------------------------------- + find_homelab_directory() { + local POSSIBLE_DIRS=( + "$HOME/homelab" + "/home/*/homelab" + "/opt/homelab" + "/srv/homelab" + ) + + # Try to find from running user's home first + if [[ -n "$SUDO_USER" ]]; then + local user_home=$(eval echo "~$SUDO_USER") + POSSIBLE_DIRS=("$user_home/homelab" "${POSSIBLE_DIRS[@]}") + fi + + HOMELAB_DIR="" + for dir in "${POSSIBLE_DIRS[@]}"; do + if [[ -f "$dir/docker-compose.yml" ]]; then + HOMELAB_DIR="$dir" + log "โœ… Found homelab directory: $HOMELAB_DIR" + break + fi + done + + if [[ -z "$HOMELAB_DIR" ]]; then + echo -e "\n๐Ÿ“‚ Could not auto-detect homelab directory." + echo -e "Please enter the path to your homelab directory (contains docker-compose.yml):" + read -r user_dir + + if [[ -f "$user_dir/docker-compose.yml" ]]; then + HOMELAB_DIR="$user_dir" + log "โœ… Using homelab directory: $HOMELAB_DIR" + else + warning "No docker-compose.yml found in $user_dir" + log "๐Ÿ“ Will proceed with container cleanup by name instead" + fi + fi + + # Set APPDATA_DIR from env file if available + if [[ -f "$HOMELAB_DIR/.env" ]]; then + APPDATA_DIR=$(grep "^CONFIG_ROOT=" "$HOMELAB_DIR/.env" | cut -d= -f2) + log "๐Ÿ“ Found appdata directory: $APPDATA_DIR" + fi + } + + # -------------------------------------------- + # SERVICE DETECTION + # -------------------------------------------- + detect_running_services() { + log "๐Ÿ” Detecting running HOPS services..." + + # Known HOPS service names + local KNOWN_SERVICES=( + "sonarr" "radarr" "lidarr" "readarr" "bazarr" "prowlarr" "tdarr" + "nzbget" "sabnzbd" "transmission" "qbittorrent" + "plex" "jellyfin" "emby" "jellystat" "jellystat-db" + "overseerr" "jellyseerr" "ombi" + "traefik" "nginx-proxy-manager" "authelia" + "portainer" "watchtower" "uptime-kuma" + "postgres" "redis" + ) + + DETECTED_SERVICES=() + for service in "${KNOWN_SERVICES[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${service}$"; then + DETECTED_SERVICES+=("$service") + fi + done + + if [[ ${#DETECTED_SERVICES[@]} -gt 0 ]]; then + log "โœ… Detected services: ${DETECTED_SERVICES[*]}" + else + log "โš ๏ธ No HOPS services detected" + fi + } + + # -------------------------------------------- + # CONTAINER CLEANUP + # -------------------------------------------- + stop_and_remove_containers() { + log "๐Ÿ›‘ Stopping and removing containers..." + + if [[ -n "$HOMELAB_DIR" && -f "$HOMELAB_DIR/docker-compose.yml" ]]; then + cd "$HOMELAB_DIR" + + # Stop services gracefully + log "๐Ÿ”„ Stopping services with docker compose..." + if docker compose ps -q | grep -q .; then + if ! docker compose down --timeout 30 2>&1 | tee -a "$LOG_FILE"; then + warning "Docker compose down failed, attempting force removal" + docker compose down --timeout 10 --remove-orphans --volumes 2>&1 | tee -a "$LOG_FILE" || true + fi + else + log "โ„น๏ธ No running compose services found" + fi + fi + + # Fallback: Remove containers by name + if [[ ${#DETECTED_SERVICES[@]} -gt 0 ]]; then + log "๐Ÿงน Cleaning up remaining containers..." + for service in "${DETECTED_SERVICES[@]}"; do + if docker ps -a --format "{{.Names}}" | grep -q "^${service}$"; then + log "๐Ÿ—‘๏ธ Removing container: $service" + docker stop "$service" 2>/dev/null || true + docker rm -f "$service" 2>/dev/null || true + fi + done + fi + + log "โœ… Container cleanup complete" + } + + # -------------------------------------------- + # IMAGE CLEANUP + # -------------------------------------------- + remove_docker_images() { + if [[ $REMOVE_IMAGES == true ]]; then + log "๐Ÿ—‘๏ธ Removing Docker images..." + + # Common HOPS images + local HOPS_IMAGES=( + "lscr.io/linuxserver/sonarr" + "lscr.io/linuxserver/radarr" + "lscr.io/linuxserver/lidarr" + "lscr.io/linuxserver/readarr" + "lscr.io/linuxserver/bazarr" + "lscr.io/linuxserver/prowlarr" + "ghcr.io/haveagitgat/tdarr" + "lscr.io/linuxserver/nzbget" + "lscr.io/linuxserver/sabnzbd" + "lscr.io/linuxserver/transmission" + "lscr.io/linuxserver/qbittorrent" + "plexinc/pms-docker" + "jellyfin/jellyfin" + "emby/embyserver" + "sctx/overseerr" + "fallenbagel/jellyseerr" + "lscr.io/linuxserver/ombi" + "traefik" + "jc21/nginx-proxy-manager" + "authelia/authelia" + "portainer/portainer-ce" + "containrrr/watchtower" + "louislam/uptime-kuma" + "postgres" + "redis" + "cyfershepard/jellystat" + ) + + for image in "${HOPS_IMAGES[@]}"; do + if docker images --format "{{.Repository}}" | grep -q "^${image}$"; then + log "๐Ÿ—‘๏ธ Removing image: $image" + docker rmi -f "$image" 2>/dev/null || true + fi + done + + # Clean up dangling images + log "๐Ÿงน Cleaning up dangling images..." + docker image prune -f 2>/dev/null || true + + log "โœ… Image cleanup complete" + else + log "โญ๏ธ Skipping image removal" + fi + } + + # -------------------------------------------- + # NETWORK CLEANUP + # -------------------------------------------- + cleanup_networks() { + log "๐ŸŒ Cleaning up Docker networks..." + + local HOPS_NETWORKS=("homelab" "traefik" "database") + + for network in "${HOPS_NETWORKS[@]}"; do + if docker network ls --format "{{.Name}}" | grep -q "^${network}$"; then + log "๐Ÿ—‘๏ธ Removing network: $network" + docker network rm "$network" 2>/dev/null || warning "Could not remove network: $network" + fi + done + + log "โœ… Network cleanup complete" + } + + # -------------------------------------------- + # VOLUME CLEANUP + # -------------------------------------------- + cleanup_volumes() { + log "๐Ÿ’พ Cleaning up Docker volumes..." + + local HOPS_VOLUMES=("postgres_data" "redis_data") + + for volume in "${HOPS_VOLUMES[@]}"; do + if docker volume ls --format "{{.Name}}" | grep -q "^${volume}$"; then + log "๐Ÿ—‘๏ธ Removing volume: $volume" + docker volume rm "$volume" 2>/dev/null || warning "Could not remove volume: $volume" + fi + done + + # Clean up orphaned volumes + log "๐Ÿงน Cleaning up orphaned volumes..." + docker volume prune -f 2>/dev/null || true + + log "โœ… Volume cleanup complete" + } + + # -------------------------------------------- + # FILE CLEANUP + # -------------------------------------------- + cleanup_compose_files() { + if [[ $REMOVE_COMPOSE == true && -n "$HOMELAB_DIR" ]]; then + log "๐Ÿ“ Removing Docker Compose files..." + + cd "$HOMELAB_DIR" + + # Backup before removal + if [[ -f docker-compose.yml ]]; then + local BACKUP_FILE="docker-compose.yml.removed.$(date +%Y%m%d%H%M%S)" + log "๐Ÿ“ฆ Backing up compose file to: $BACKUP_FILE" + cp docker-compose.yml "$BACKUP_FILE" 2>/dev/null || warning "Could not backup compose file" + rm -f docker-compose.yml + fi + + # Remove other compose-related files + rm -f docker-compose.override.yml .env 2>/dev/null || true + + # Remove empty homelab directory if it's empty + cd .. + if [[ -d "$HOMELAB_DIR" ]]; then + rmdir "$HOMELAB_DIR" 2>/dev/null && log "๐Ÿ“ Removed empty homelab directory" || log "๐Ÿ“ Homelab directory not empty, keeping it" + fi + + log "โœ… Compose file cleanup complete" + else + log "โญ๏ธ Skipping compose file removal" + fi + } + + cleanup_appdata() { + if [[ $REMOVE_APPDATA == true && -n "$APPDATA_DIR" ]]; then + log "๐Ÿ—‚๏ธ Removing application data..." + + echo -e "\nโš ๏ธ FINAL WARNING: This will delete ALL application configurations!" + echo -e "Application data directory: $APPDATA_DIR" + echo -e "โ“ Are you absolutely sure? Type 'DELETE' to confirm: " + read -r delete_confirm + + if [[ "$delete_confirm" == "DELETE" ]]; then + # Create a backup first + local BACKUP_DIR="/tmp/hops-appdata-backup-$(date +%Y%m%d%H%M%S)" + log "๐Ÿ“ฆ Creating backup at: $BACKUP_DIR" + + if cp -r "$APPDATA_DIR" "$BACKUP_DIR" 2>/dev/null; then + log "โœ… Backup created successfully" + + # Remove the original + if rm -rf "$APPDATA_DIR" 2>/dev/null; then + log "โœ… Application data removed" + log "๐Ÿ“ฆ Backup available at: $BACKUP_DIR" + else + warning "Failed to remove application data directory" + fi + else + warning "Could not create backup, skipping appdata removal" + fi + else + log "๐Ÿšซ Application data removal cancelled" + fi + else + log "โญ๏ธ Skipping application data removal" + fi + } + + # -------------------------------------------- + # FIREWALL CLEANUP + # -------------------------------------------- + cleanup_firewall() { + if [[ $REMOVE_FIREWALL == true ]]; then + log "๐Ÿ”ฅ Removing firewall rules..." + + if command -v ufw &>/dev/null; then + # Remove HOPS-specific rules by searching for comments + local rules_to_remove=() + + # Get numbered rules that contain HOPS service names + mapfile -t rules_to_remove < <(ufw status numbered | grep -E "(sonarr|radarr|lidarr|readarr|bazarr|prowlarr|jellyfin|plex|portainer|traefik|npm)" | awk '{print $1}' | tr -d '[]') + + # Remove rules in reverse order to maintain numbering + if [[ ${#rules_to_remove[@]} -gt 0 ]]; then + for ((i=${#rules_to_remove[@]}-1; i>=0; i--)); do + local rule_num="${rules_to_remove[i]}" + log "๐Ÿ—‘๏ธ Removing firewall rule #$rule_num" + echo "y" | ufw delete "$rule_num" 2>/dev/null || true + done + fi + + log "โœ… Firewall cleanup complete" + else + log "โ„น๏ธ UFW not installed, skipping firewall cleanup" + fi + else + log "โญ๏ธ Skipping firewall cleanup" + fi + } + + # -------------------------------------------- + # DOCKER UNINSTALLATION + # -------------------------------------------- + uninstall_docker() { + if [[ $REMOVE_DOCKER == true ]]; then + log "๐Ÿณ Uninstalling Docker..." + + # Stop Docker service + systemctl stop docker 2>/dev/null || true + systemctl disable docker 2>/dev/null || true + + # Remove Docker packages + local DOCKER_PACKAGES=( + "docker-ce" "docker-ce-cli" "containerd.io" "docker-buildx-plugin" + "docker-compose-plugin" "docker.io" "docker-doc" "docker-compose" + "podman-docker" "containerd" "runc" + ) + + for package in "${DOCKER_PACKAGES[@]}"; do + if dpkg -l | grep -q "^ii.*$package"; then + log "๐Ÿ—‘๏ธ Removing package: $package" + apt-get remove -y "$package" 2>/dev/null || true + fi + done + + # Clean up Docker directories + log "๐Ÿ—‘๏ธ Removing Docker directories..." + rm -rf /var/lib/docker /etc/docker /var/run/docker.sock 2>/dev/null || true + rm -rf ~/.docker 2>/dev/null || true + + # Remove from user directories too + if [[ -n "$SUDO_USER" ]]; then + local user_home=$(eval echo "~$SUDO_USER") + rm -rf "$user_home/.docker" 2>/dev/null || true + fi + + # Remove Docker group + if getent group docker &>/dev/null; then + groupdel docker 2>/dev/null || warning "Could not remove docker group" + fi + + # Clean up package cache + apt-get autoremove -y 2>/dev/null || true + apt-get autoclean 2>/dev/null || true + + log "โœ… Docker uninstallation complete" + else + log "โญ๏ธ Skipping Docker uninstallation" + fi + } + + # -------------------------------------------- + # CLEANUP LOG FILES + # -------------------------------------------- + cleanup_logs() { + log "๐Ÿ“‹ Cleaning up HOPS log files..." + + # Keep current log file, remove others + find "$LOG_DIR" -name "homelab-*.log" -not -name "$(basename "$LOG_FILE")" -delete 2>/dev/null || true + + # Remove log directory if empty (except current log) + local remaining_logs=$(find "$LOG_DIR" -name "*.log" | wc -l) + if [[ $remaining_logs -le 1 ]]; then + log "๐Ÿ“ Log directory will be cleaned after this session" + fi + + log "โœ… Log cleanup complete" + } + + # -------------------------------------------- + # MAIN UNINSTALLATION FLOW + # -------------------------------------------- + show_uninstall_warning + get_uninstall_options + + log "๐Ÿš€ Starting HOPS uninstallation process..." + + find_homelab_directory + detect_running_services + + # Core cleanup steps + stop_and_remove_containers + remove_docker_images + cleanup_networks + cleanup_volumes + cleanup_compose_files + cleanup_appdata + cleanup_firewall + uninstall_docker + cleanup_logs + + # -------------------------------------------- + # FINAL SUMMARY + # -------------------------------------------- + echo -e "\nโœ… HOPS Uninstallation Complete!" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + + log "๐Ÿ“‹ Uninstallation Summary:" + echo -e "\n๐Ÿ—‘๏ธ Removed Components:" + echo " โ€ข Docker containers: โœ…" + echo " โ€ข Docker Compose files: โœ…" + [[ $REMOVE_IMAGES == true ]] && echo " โ€ข Docker images: โœ…" || echo " โ€ข Docker images: โŒ (kept)" + [[ $REMOVE_APPDATA == true ]] && echo " โ€ข Application data: โœ…" || echo " โ€ข Application data: โŒ (kept)" + [[ $REMOVE_DOCKER == true ]] && echo " โ€ข Docker installation: โœ…" || echo " โ€ข Docker installation: โŒ (kept)" + [[ $REMOVE_FIREWALL == true ]] && echo " โ€ข Firewall rules: โœ…" || echo " โ€ข Firewall rules: โŒ (kept)" + + echo -e "\n๐Ÿ“‚ Preserved:" + echo " โ€ข Media files: โœ… (never touched)" + [[ $REMOVE_APPDATA != true ]] && echo " โ€ข Application configurations: โœ…" + + if [[ $REMOVE_APPDATA == true ]]; then + echo -e "\n๐Ÿ“ฆ Backup Location:" + echo " โ€ข Application data backup: /tmp/hops-appdata-backup-*" + echo " โ€ข Consider moving this backup to a permanent location" + fi + + echo -e "\n๐Ÿ“‹ Complete log: $LOG_FILE" + echo -e "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + + log "โœ… HOPS uninstallation completed successfully!" + + if [[ -n "$SUDO_USER" ]]; then + echo -e "\n๐Ÿ’ก Note: You may want to restart your session to ensure all group changes take effect." + fi + + if [[ $REMOVE_DOCKER == true ]]; then + echo -e "\n๐Ÿ”„ Recommendation: Reboot your system to complete Docker removal." + fi + + return 0 +} \ No newline at end of file