Initial release of HOPS v3.1.0

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Stephen Klein
2025-07-12 06:42:48 -04:00
commit 772fb7da52
10 changed files with 7257 additions and 0 deletions
Executable
+679
View File
@@ -0,0 +1,679 @@
#!/bin/bash
# HOPS - Homelab Orchestration Provisioning Script
# Primary Management Script
# Version: 3.1.0
# Exit on any error
set -e
# Script version and metadata
readonly SCRIPT_VERSION="3.1.0"
readonly SCRIPT_NAME="HOPS"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default script locations
readonly INSTALLER_SCRIPT="$SCRIPT_DIR/hops_installer_enhanced.sh"
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/hops_uninstaller_fixed.sh"
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/hops_service_definitions.sh"
# Color codes for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly PURPLE='\033[0;35m'
readonly CYAN='\033[0;36m'
readonly WHITE='\033[1;37m'
readonly NC='\033[0m' # No Color
# Logging setup
readonly LOG_DIR="/var/log/hops"
readonly LOG_FILE="$LOG_DIR/hops-main-$(date +%Y%m%d-%H%M%S).log"
# Initialize logging
init_logging() {
if [[ $EUID -eq 0 ]]; then
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
fi
}
# Logging function
log() {
local message="$1"
local timestamp="$(date '+%Y-%m-%d %T')"
if [[ -w "$LOG_FILE" ]]; then
echo "$timestamp - $message" >> "$LOG_FILE"
fi
echo -e "$message"
}
# Error handling
error_exit() {
log "${RED}❌ ERROR: $1${NC}"
exit 1
}
# Warning function
warning() {
log "${YELLOW}⚠️ WARNING: $1${NC}"
}
# Success function
success() {
log "${GREEN}$1${NC}"
}
# Info function
info() {
log "${BLUE}$1${NC}"
}
# Clear screen and show header
show_header() {
clear
cat << "EOF"
_ _ ____ ____ ____
| | | || _ \| _ \/ ___|
| |__| || |_) | |_) \___ \
| __ || __/| __/ ___) |
|_| |_||_| |_| |____/
EOF
echo -e "${CYAN}🚀 Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}${NC}"
echo -e "${WHITE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root or with sudo."
fi
}
# Validate script dependencies
check_dependencies() {
local missing_deps=()
# Check for required scripts
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
missing_deps+=("Installer script: $INSTALLER_SCRIPT")
fi
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
missing_deps+=("Uninstaller script: $UNINSTALLER_SCRIPT")
fi
if [[ ! -f "$SERVICE_DEFINITIONS" ]]; then
missing_deps+=("Service definitions: $SERVICE_DEFINITIONS")
fi
# Check for required commands
local required_commands=("curl" "wget" "systemctl")
for cmd in "${required_commands[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
missing_deps+=("Command: $cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
error_exit "Missing dependencies:\n$(printf ' • %s\n' "${missing_deps[@]}")"
fi
}
# Check system requirements
check_system_requirements() {
info "Checking system requirements..."
# Check OS
if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then
warning "This script is designed for Ubuntu/Debian/Mint systems"
echo -e "Continue anyway? [y/N]: "
read -r continue_choice
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 0
fi
# Check minimum requirements
local ram_gb=$(free -g | awk '/^Mem:/{print $2}')
local disk_gb=$(df -BG --output=avail / | tail -n 1 | tr -d 'G')
if [[ $ram_gb -lt 2 ]]; then
warning "Low RAM detected: ${ram_gb}GB (2GB+ recommended)"
fi
if [[ $disk_gb -lt 10 ]]; then
warning "Low disk space: ${disk_gb}GB (10GB+ recommended)"
fi
success "System requirements check complete"
}
# Get HOPS installation status
get_installation_status() {
local status="not_installed"
local homelab_dirs=(
"$HOME/homelab"
"/home/*/homelab"
"/opt/homelab"
"/srv/homelab"
)
# Check for existing installation
for dir in "${homelab_dirs[@]}"; do
if [[ -f "$dir/docker-compose.yml" ]]; then
status="installed"
HOMELAB_DIR="$dir"
break
fi
done
# Check for running containers
if command -v docker &>/dev/null && docker ps --format "{{.Names}}" | grep -qE "(sonarr|radarr|jellyfin|plex|portainer)"; then
if [[ "$status" == "not_installed" ]]; then
status="partial"
else
status="running"
fi
fi
echo "$status"
}
# Show installation status
show_status() {
local status=$(get_installation_status)
echo -e "${WHITE}📊 Current Status:${NC}"
case "$status" in
"not_installed")
echo -e " ${RED}● Not Installed${NC}"
;;
"installed")
echo -e " ${YELLOW}● Installed (stopped)${NC}"
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
;;
"running")
echo -e " ${GREEN}● Running${NC}"
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
;;
"partial")
echo -e " ${YELLOW}● Partial Installation${NC}"
;;
esac
echo
}
# Run installer
run_installer() {
info "Launching HOPS installer..."
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
error_exit "Installer script not found: $INSTALLER_SCRIPT"
fi
# Source the installer function and run it
if source "$INSTALLER_SCRIPT" && install_hops; then
success "Installation completed successfully!"
else
error_exit "Installation failed. Check logs for details."
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Run uninstaller
run_uninstaller() {
info "Launching HOPS uninstaller..."
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
error_exit "Uninstaller script not found: $UNINSTALLER_SCRIPT"
fi
# Source the uninstaller function and run it
if source "$UNINSTALLER_SCRIPT" && uninstall_hops; then
success "Uninstallation completed successfully!"
else
error_exit "Uninstallation failed. Check logs for details."
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show service status
show_service_status() {
show_header
echo -e "${WHITE}🔍 Service Status Check${NC}\n"
if ! command -v docker &>/dev/null; then
warning "Docker is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}📊 Docker Service Status:${NC}"
if systemctl is-active --quiet docker; then
echo -e " ${GREEN}● Docker daemon: Running${NC}"
else
echo -e " ${RED}● Docker daemon: Stopped${NC}"
fi
echo -e "\n${BLUE}📦 Container Status:${NC}"
# Source service definitions to get port info
if [[ -f "$SERVICE_DEFINITIONS" ]]; then
source "$SERVICE_DEFINITIONS"
fi
# Known HOPS services with their ports
local services=(
"sonarr:8989" "radarr:7878" "lidarr:8686" "readarr:8787"
"bazarr:6767" "prowlarr:9696" "jellyfin:8096" "plex:32400"
"overseerr:5055" "jellyseerr:5056" "portainer:9000"
"traefik:8080" "nginx-proxy-manager:81" "qbittorrent:8082"
"transmission:9091" "nzbget:6789" "sabnzbd:8080"
"uptime-kuma:3001" "jellystat:3000"
)
local running_count=0
local total_count=0
for service_info in "${services[@]}"; do
local service_name="${service_info%:*}"
local service_port="${service_info#*:}"
if docker ps --format "{{.Names}}" | grep -q "^${service_name}$"; then
((total_count++))
local status_symbol="${GREEN}${NC}"
local status_text="Running"
# Check if port is accessible
if curl -sSf --max-time 2 --connect-timeout 1 "http://localhost:${service_port}" >/dev/null 2>&1; then
status_text="Running & Accessible"
((running_count++))
else
status_text="Running (starting up)"
status_symbol="${YELLOW}${NC}"
fi
printf " %s %-20s %s (:%s)\n" "$status_symbol" "$service_name" "$status_text" "$service_port"
elif docker ps -a --format "{{.Names}}" | grep -q "^${service_name}$"; then
((total_count++))
printf " %s %-20s %s\n" "${RED}${NC}" "$service_name" "Stopped"
fi
done
if [[ $total_count -eq 0 ]]; then
echo -e " ${YELLOW}No HOPS services found${NC}"
else
echo -e "\n${WHITE}📈 Summary: ${running_count}/${total_count} services running and accessible${NC}"
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Manage services (start/stop/restart)
manage_services() {
show_header
echo -e "${WHITE}🎛️ Service Management${NC}\n"
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
if [[ -z "$HOMELAB_DIR" ]]; then
error_exit "Cannot locate homelab directory with docker-compose.yml"
fi
echo -e "${BLUE}Available actions:${NC}"
echo -e " 1) Start all services"
echo -e " 2) Stop all services"
echo -e " 3) Restart all services"
echo -e " 4) View logs (recent)"
echo -e " 5) View logs (follow)"
echo -e " 6) Update services"
echo -e " 7) Restart individual service"
echo -e " 8) Back to main menu"
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
read -r choice
cd "$HOMELAB_DIR"
case $choice in
1)
info "Starting all services..."
if docker compose up -d; then
success "Services started"
else
warning "Some services may have failed to start"
fi
;;
2)
info "Stopping all services..."
if docker compose down; then
success "Services stopped"
else
warning "Some services may not have stopped cleanly"
fi
;;
3)
info "Restarting all services..."
if docker compose restart; then
success "Services restarted"
else
warning "Some services may have failed to restart"
fi
;;
4)
info "Showing recent logs..."
docker compose logs --tail=100
;;
5)
info "Following logs (Ctrl+C to exit)..."
docker compose logs -f --tail=50
;;
6)
info "Updating services..."
if docker compose pull && docker compose up -d; then
success "Services updated"
else
warning "Update may have failed"
fi
;;
7)
echo -e "\n${WHITE}Available services:${NC}"
docker compose ps --services | nl -w2 -s') '
echo -e "\n${WHITE}Enter service name to restart: ${NC}"
read -r service_name
if [[ -n "$service_name" ]]; then
info "Restarting $service_name..."
if docker compose restart "$service_name"; then
success "$service_name restarted"
else
warning "Failed to restart $service_name"
fi
fi
;;
8)
return
;;
*)
warning "Invalid option"
;;
esac
echo -e "\n${WHITE}Press Enter to continue...${NC}"
read -r
}
# Show quick access URLs
show_access_info() {
show_header
echo -e "${WHITE}🌐 Service Access Information${NC}\n"
local status=$(get_installation_status)
if [[ "$status" == "not_installed" ]]; then
warning "HOPS is not installed"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}📱 Access your services at:${NC}"
# Get local IP
local local_ip=$(hostname -I | awk '{print $1}')
# Service URLs with paths
local services=(
"Sonarr:8989:/sonarr"
"Radarr:7878:/radarr"
"Lidarr:8686:/lidarr"
"Readarr:8787:/readarr"
"Bazarr:6767:"
"Prowlarr:9696:"
"Jellyfin:8096:"
"Plex:32400:/web"
"Overseerr:5055:"
"Jellyseerr:5056:"
"Portainer:9000:"
"Traefik:8080:"
"NPM:81:"
"qBittorrent:8082:"
"Transmission:9091:"
"NZBGet:6789:"
"SABnzbd:8080:"
"Uptime-Kuma:3001:"
"Jellystat:3000:"
)
local active_services=0
for service_info in "${services[@]}"; do
local service_name="${service_info%%:*}"
local service_port="${service_info#*:}"
local service_path="${service_port#*:}"
service_port="${service_port%:*}"
if docker ps --format "{{.Names}}" | grep -qi "${service_name,,}"; then
local url="http://${local_ip}:${service_port}${service_path}"
printf " ${GREEN}${NC} %-15s %s\n" "$service_name" "$url"
((active_services++))
fi
done
if [[ $active_services -eq 0 ]]; then
echo -e " ${YELLOW}No services currently running${NC}"
fi
echo -e "\n${YELLOW}💡 Tips:${NC}"
echo -e " • Bookmark these URLs for easy access"
echo -e " • Default credentials are in the .env file"
echo -e " • Change default passwords after first login"
echo -e " • Some services may take a few minutes to fully start"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show logs
show_logs() {
show_header
echo -e "${WHITE}📋 HOPS Logs${NC}\n"
if [[ ! -d "$LOG_DIR" ]]; then
warning "No log directory found"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
echo -e "${BLUE}Available log files:${NC}"
local log_files=($(find "$LOG_DIR" -name "*.log" -type f | sort -r))
if [[ ${#log_files[@]} -eq 0 ]]; then
warning "No log files found"
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
return
fi
local count=1
for log_file in "${log_files[@]}"; do
local basename_log=$(basename "$log_file")
local size=$(du -h "$log_file" | cut -f1)
local date=$(stat -c %y "$log_file" | cut -d' ' -f1)
printf " %d) %-40s (%s, %s)\n" "$count" "$basename_log" "$size" "$date"
((count++))
done
echo -e "\n${WHITE}Select a log file to view [1-${#log_files[@]}] or 0 to go back: ${NC}"
read -r choice
if [[ "$choice" -eq 0 ]]; then
return
elif [[ "$choice" -gt 0 && "$choice" -le ${#log_files[@]} ]]; then
local selected_log="${log_files[$((choice-1))]}"
echo -e "\n${BLUE}Showing last 50 lines of $(basename "$selected_log"):${NC}\n"
tail -50 "$selected_log"
else
warning "Invalid selection"
fi
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Show help information
show_help() {
show_header
echo -e "${WHITE}📚 HOPS Help & Documentation${NC}\n"
echo -e "${BLUE}🎯 What is HOPS?${NC}"
echo -e "HOPS (Homelab Orchestration Provisioning Script) is an automated installer"
echo -e "for popular homelab applications including media servers, download clients,"
echo -e "and monitoring tools.\n"
echo -e "${BLUE}🚀 Quick Start:${NC}"
echo -e " 1. Run this script as root/sudo"
echo -e " 2. Choose 'Install HOPS' from the menu"
echo -e " 3. Configure directories and timezone"
echo -e " 4. Select your desired services"
echo -e " 5. Wait for installation to complete"
echo -e " 6. Access services via the provided URLs\n"
echo -e "${BLUE}📱 Supported Services:${NC}"
echo -e " • Media Management: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr"
echo -e " • Download Clients: qBittorrent, Transmission, NZBGet, SABnzbd"
echo -e " • Media Servers: Jellyfin, Plex, Emby, Jellystat"
echo -e " • Request Management: Overseerr, Jellyseerr, Ombi"
echo -e " • Reverse Proxy: Traefik, Nginx Proxy Manager"
echo -e " • Monitoring: Portainer, Uptime Kuma, Watchtower\n"
echo -e "${BLUE}🔧 Requirements:${NC}"
echo -e " • Ubuntu/Debian/Mint Linux"
echo -e " • 2GB+ RAM (4GB+ recommended)"
echo -e " • 10GB+ free disk space"
echo -e " • Root/sudo access"
echo -e " • Internet connection\n"
echo -e "${BLUE}📁 Default Locations:${NC}"
echo -e " • Homelab directory: ~/homelab/"
echo -e " • App configurations: /opt/appdata/"
echo -e " • Media storage: /mnt/media/"
echo -e " • Logs: /var/log/hops/\n"
echo -e "${BLUE}🆘 Troubleshooting:${NC}"
echo -e " • Check logs in the 'View Logs' menu"
echo -e " • Verify Docker is running: systemctl status docker"
echo -e " • Check container status: docker ps"
echo -e " • View service logs: docker logs [service-name]"
echo -e " • Restart services: docker compose restart [service-name]\n"
echo -e "${BLUE}🔐 Security Notes:${NC}"
echo -e " • Change default passwords in .env file after installation"
echo -e " • Configure firewall rules as needed"
echo -e " • Regularly update services using the management menu\n"
echo -e "${WHITE}Press Enter to return to main menu...${NC}"
read -r
}
# Main menu
show_main_menu() {
local status=$(get_installation_status)
echo -e "${WHITE}🎛️ Main Menu:${NC}"
echo -e " 1) Install HOPS"
if [[ "$status" != "not_installed" ]]; then
echo -e " 2) Uninstall HOPS"
echo -e " 3) Manage Services"
echo -e " 4) Service Status"
echo -e " 5) Access Information"
else
echo -e " 2) Uninstall HOPS ${YELLOW}(not installed)${NC}"
echo -e " 3) Manage Services ${YELLOW}(not installed)${NC}"
echo -e " 4) Service Status ${YELLOW}(not installed)${NC}"
echo -e " 5) Access Information ${YELLOW}(not installed)${NC}"
fi
echo -e " 6) View Logs"
echo -e " 7) Help & Documentation"
echo -e " 8) Exit"
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
}
# Main program loop
main() {
init_logging
check_root
check_dependencies
while true; do
show_header
show_status
show_main_menu
read -r choice
case $choice in
1)
check_system_requirements
run_installer
;;
2)
run_uninstaller
;;
3)
manage_services
;;
4)
show_service_status
;;
5)
show_access_info
;;
6)
show_logs
;;
7)
show_help
;;
8)
info "Thank you for using HOPS!"
exit 0
;;
*)
warning "Invalid option. Please select 1-8."
sleep 2
;;
esac
done
}
# Script entry point
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi