#!/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