Rename scripts for clarity and add Huntarr service

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Stephen Klein
2025-07-17 21:52:22 -04:00
parent 721f7d7a75
commit 5affcd2e26
17 changed files with 1077 additions and 3678 deletions
Executable
+935
View File
@@ -0,0 +1,935 @@
#!/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)"
# Load system utilities
source "$SCRIPT_DIR/lib/common.sh"
source "$SCRIPT_DIR/lib/system.sh"
# --------------------------------------------
# LOGGING SETUP
# --------------------------------------------
setup_logging "homelab-setup"
local_error_exit() {
error_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
# --------------------------------------------
validate_system_requirements() {
local MIN_RAM_GB=2
local MIN_DISK_GB=10
local MIN_CORES=2
info "🔍 Validating system requirements..."
# Detect OS first
detect_os
# Check system requirements using new abstraction
check_system_requirements $MIN_RAM_GB $MIN_DISK_GB
return 0
}
# --------------------------------------------
# REQUIRED PACKAGES CHECK
# --------------------------------------------
check_required_packages() {
local missing_packages=()
local required_packages=("curl" "wget" "openssl" "lsof")
# Add OS-specific packages
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
required_packages+=("httpd") # Apache on macOS (for htpasswd)
else
required_packages+=("apache2-utils") # Apache utils on Linux
fi
info "📦 Checking required packages..."
for package in "${required_packages[@]}"; do
local check_cmd="${package%%-*}"
if [[ "$package" == "httpd" ]]; then
check_cmd="htpasswd" # Check for htpasswd command on macOS
fi
if ! command -v "$check_cmd" &>/dev/null; then
missing_packages+=("$package")
fi
done
if [[ ${#missing_packages[@]} -gt 0 ]]; then
info "📦 Installing missing packages: ${missing_packages[*]}"
for package in "${missing_packages[@]}"; do
install_package "$package"
done
fi
success "✅ 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 is handled by the lib/system.sh functions
# --------------------------------------------
# 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"
local default_media_path=$(get_default_media_path)
local default_config_path=$(get_default_config_path)
echo -e "Media directory [$default_media_path]: "
read -r media_dir
MEDIA_DIR="${media_dir:-$default_media_path}"
echo -e "Application data directory [$default_config_path]: "
read -r appdata_dir
APPDATA_DIR="${appdata_dir:-$default_config_path}"
# 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/services" ]]; then
source "$SCRIPT_DIR/services"
fi
for svc in "${SERVICES[@]}"; do
local ports=$(get_service_ports "$svc")
for port in $ports; do
if ! check_port "$port" "$svc"; then
CONFLICTS+=("Port $port ($svc)")
fi
done
done
if [[ ${#CONFLICTS[@]} -gt 0 ]]; then
log "⚠️ Found ${#CONFLICTS[@]} port conflicts:"
for conflict in "${CONFLICTS[@]}"; do
log " • $conflict"
done
echo -e "\n⚠️ Port conflicts detected! Continue anyway? (y/N): "
read -r continue_choice
if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then
error_exit "Installation cancelled due to port conflicts."
fi
return 1
fi
return 0
}
# --------------------------------------------
# DOCKER COMPOSE VERSION CHECK
# --------------------------------------------
check_docker_compose_version() {
# Check for Docker Compose plugin (v2)
if docker compose version &>/dev/null; then
log "✅ Docker Compose plugin detected ($(docker compose version --short))"
return 0
fi
# Check for standalone docker-compose (v1)
if command -v docker-compose &>/dev/null; then
log "⚠️ Found legacy docker-compose (v1). Installing Docker Compose plugin..."
if ! apt-get install -y docker-compose-plugin 2>&1 | tee -a "$LOG_FILE"; then
error_exit "Failed to install Docker Compose plugin."
fi
return 0
fi
# Neither found
error_exit "No Docker Compose detected. Please install Docker first."
}
# --------------------------------------------
# IMPROVED PASSWORD GENERATION
# --------------------------------------------
generate_secure_password() {
local length="${1:-16}"
local max_attempts=5
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
# Generate password with mixed case, numbers, and symbols
local password=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-${length})
# Ensure it meets complexity requirements
if validate_password "$password" "$length"; then
echo "$password"
return 0
fi
((attempt++))
done
# Fallback: construct a guaranteed compliant password
local upper=$(tr -dc 'A-Z' < /dev/urandom | head -c2)
local lower=$(tr -dc 'a-z' < /dev/urandom | head -c4)
local digits=$(tr -dc '0-9' < /dev/urandom | head -c2)
local symbols=$(tr -dc '!@#$%^&*' < /dev/urandom | head -c2)
local remaining_length=$((length - 10))
if [[ $remaining_length -gt 0 ]]; then
local remaining=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c$remaining_length)
echo "${upper}${lower}${digits}${symbols}${remaining}" | fold -w1 | shuf | tr -d '\n'
else
echo "${upper}${lower}${digits}${symbols}"
fi
}
# --------------------------------------------
# ENVIRONMENT FILE GENERATION
# --------------------------------------------
create_env_file() {
local homelab_dir="$1"
log "📝 Creating environment file..."
cat > "$homelab_dir/.env" <<EOF
# HOPS Environment Configuration
# Generated on $(date)
# ==============================================
# CORE CONFIGURATION
# ==============================================
# User Configuration
PUID=$PUID
PGID=$PGID
TZ=$TIMEZONE
# Directory Configuration
DATA_ROOT=$MEDIA_DIR
CONFIG_ROOT=$APPDATA_DIR
HOMELAB_DIR=$homelab_dir
# Network Configuration
DOCKER_SUBNET=172.20.0.0/16
# ==============================================
# SECURITY & AUTHENTICATION
# ==============================================
# Default Passwords (CHANGE THESE IMMEDIATELY!)
DEFAULT_ADMIN_PASSWORD=$(generate_secure_password 16)
DEFAULT_DB_PASSWORD=$(generate_secure_password 20)
# Optional: Custom domain for reverse proxy
# DOMAIN=yourdomain.com
# Optional: Email for Let's Encrypt
# ACME_EMAIL=admin@yourdomain.com
# ==============================================
# SERVICE-SPECIFIC CONFIGURATION
# ==============================================
# Plex Configuration (Get token from: https://www.plex.tv/claim/)
PLEX_CLAIM_TOKEN=
# Watchtower Email Notifications (Optional)
WATCHTOWER_EMAIL_FROM=
WATCHTOWER_EMAIL_TO=
WATCHTOWER_EMAIL_SERVER=
WATCHTOWER_EMAIL_PORT=587
WATCHTOWER_EMAIL_USER=
WATCHTOWER_EMAIL_PASSWORD=
# Traefik Let's Encrypt Email
ACME_EMAIL=admin@localhost
EOF
chmod 600 "$homelab_dir/.env"
log "✅ Environment file created with secure permissions"
}
# --------------------------------------------
# SERVICE SELECTION
# --------------------------------------------
select_services() {
echo -e "\n📺 CORE MEDIA TOOLS"
echo "1) Sonarr 2) Radarr 3) Lidarr 4) Readarr"
echo "5) Bazarr 6) Prowlarr 7) Tdarr 8) Huntarr"
echo -e "\n⬇️ DOWNLOAD CLIENTS"
echo "9) NZBGet 10) SABnzbd 11) Transmission 12) qBittorrent"
echo -e "\n🎞️ MEDIA SERVERS"
echo "13) Plex 14) Jellyfin 15) Jellystat 16) Emby"
echo -e "\n🎛️ REQUEST MANAGEMENT"
echo "17) Overseerr 18) Jellyseerr 19) Ombi"
echo -e "\n🔒 NETWORK & SECURITY"
echo "20) Traefik 21) Nginx Proxy Manager 22) Authelia"
echo -e "\n📈 MONITORING"
echo "23) Portainer 24) Watchtower 25) Uptime Kuma"
echo -e "\n📝 Select services (space-separated numbers, or 'all' for everything): "
read -a service_choices
# Function to map service numbers to names (bash 3.2 compatible)
get_service_name() {
case "$1" in
1) echo "sonarr" ;;
2) echo "radarr" ;;
3) echo "lidarr" ;;
4) echo "readarr" ;;
5) echo "bazarr" ;;
6) echo "prowlarr" ;;
7) echo "tdarr" ;;
8) echo "huntarr" ;;
9) echo "nzbget" ;;
10) echo "sabnzbd" ;;
11) echo "transmission" ;;
12) echo "qbittorrent" ;;
13) echo "plex" ;;
14) echo "jellyfin" ;;
15) echo "jellystat" ;;
16) echo "emby" ;;
17) echo "overseerr" ;;
18) echo "jellyseerr" ;;
19) echo "ombi" ;;
20) echo "traefik" ;;
21) echo "nginx-proxy-manager" ;;
22) echo "authelia" ;;
23) echo "portainer" ;;
24) echo "watchtower" ;;
25) echo "uptime-kuma" ;;
*) echo "" ;;
esac
}
SERVICES=()
if [[ "${service_choices[0]}" == "all" ]]; then
SERVICES=("sonarr" "radarr" "lidarr" "readarr" "bazarr" "prowlarr" "tdarr" "huntarr" "nzbget" "sabnzbd" "transmission" "qbittorrent" "plex" "jellyfin" "jellystat" "emby" "overseerr" "jellyseerr" "ombi" "traefik" "nginx-proxy-manager" "authelia" "portainer" "watchtower" "uptime-kuma")
log "🎯 Selected all services"
else
for choice in "${service_choices[@]}"; do
service_name=$(get_service_name "$choice")
[[ -n "$service_name" ]] && SERVICES+=("$service_name")
done
fi
if [[ ${#SERVICES[@]} -eq 0 ]]; then
error_exit "No valid services selected."
fi
log "✅ Selected services: ${SERVICES[*]}"
# Check for service dependencies and conflicts
check_service_dependencies
check_service_conflicts
}
# --------------------------------------------
# DEPENDENCY AND CONFLICT CHECKING
# --------------------------------------------
check_service_dependencies() {
# Source service definitions for dependency resolution
if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/services"
# Resolve dependencies
local all_services=($(resolve_dependencies "${SERVICES[@]}"))
local deps_added=()
for service in "${all_services[@]}"; do
if [[ ! " ${SERVICES[*]} " =~ " ${service} " ]]; then
deps_added+=("$service")
fi
done
if [[ ${#deps_added[@]} -gt 0 ]]; then
log "📦 Adding dependencies: ${deps_added[*]}"
SERVICES=("${all_services[@]}")
fi
fi
# Check if any *arr services are selected without Prowlarr
local arr_services=(sonarr radarr lidarr readarr)
local has_arr=false
for arr in "${arr_services[@]}"; do
if [[ "${SERVICES[*]}" =~ $arr ]]; then
has_arr=true
break
fi
done
if [[ $has_arr == true ]] && [[ ! "${SERVICES[*]}" =~ prowlarr ]]; then
echo -e "\n💡 Recommendation: You selected *arr services but not Prowlarr."
echo "Prowlarr manages indexers for all *arr applications."
echo -e "Add Prowlarr? [Y/n]: "
read -r add_prowlarr
if [[ ! "$add_prowlarr" =~ ^[Nn]$ ]]; then
SERVICES+=("prowlarr")
log "✅ Added Prowlarr"
fi
fi
}
check_service_conflicts() {
local warnings=()
# Check for multiple media servers
local media_servers=(plex jellyfin emby)
local selected_media_servers=()
for server in "${media_servers[@]}"; do
if [[ "${SERVICES[*]}" =~ $server ]]; then
selected_media_servers+=("$server")
fi
done
if [[ ${#selected_media_servers[@]} -gt 1 ]]; then
warnings+=("Multiple media servers selected: ${selected_media_servers[*]}")
fi
# Check for multiple reverse proxies
local reverse_proxies=(traefik nginx-proxy-manager)
local selected_proxies=()
for proxy in "${reverse_proxies[@]}"; do
if [[ "${SERVICES[*]}" =~ $proxy ]]; then
selected_proxies+=("$proxy")
fi
done
if [[ ${#selected_proxies[@]} -gt 1 ]]; then
warnings+=("Multiple reverse proxies selected: ${selected_proxies[*]} (may conflict on ports 80/443)")
fi
# Display warnings if any
if [[ ${#warnings[@]} -gt 0 ]]; then
log "⚠️ Configuration warnings:"
for warning in "${warnings[@]}"; do
log " • $warning"
done
echo -e "\n⚠️ Continue with this configuration? [y/N]: "
read -r continue_choice
if [[ ! "$continue_choice" =~ ^[Yy]$ ]]; then
log "🚫 Installation cancelled by user"
exit 0
fi
fi
}
# --------------------------------------------
# DOCKER COMPOSE FILE GENERATION
# --------------------------------------------
generate_docker_compose() {
local HOMELAB_DIR="$HOME/homelab"
mkdir -p "$HOMELAB_DIR"
cd "$HOMELAB_DIR"
if [[ -f docker-compose.yml ]]; then
local BACKUP_FILE="docker-compose.yml.bak.$(date +%Y%m%d%H%M%S)"
log "📝 Backing up existing compose file to $BACKUP_FILE"
mv docker-compose.yml "$BACKUP_FILE"
fi
log "📝 Generating Docker Compose configuration..."
create_env_file "$HOMELAB_DIR"
# Source the service definitions
if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/services"
# Export variables for service definitions
export PUID PGID TIMEZONE MEDIA_DIR APPDATA_DIR
# Generate complete compose file with all services
generate_complete_compose "${SERVICES[@]}"
track_step "compose_generated"
# Create service-specific configurations
create_service_configs "${SERVICES[@]}"
log "✅ Generated Docker Compose with ${#SERVICES[@]} services"
else
error_exit "Service definitions file not found: $SCRIPT_DIR/services"
fi
# Create networks if they don't exist
create_docker_networks
}
# --------------------------------------------
# NETWORK CREATION
# --------------------------------------------
create_docker_networks() {
log "🌐 Creating Docker networks..."
# Create traefik network if it doesn't exist
if ! docker network ls --format "{{.Name}}" | grep -q "^traefik$"; then
if docker network create traefik 2>/dev/null; then
log "✅ Created traefik network"
else
log "⚠️ Could not create traefik network (may already exist)"
fi
fi
}
# --------------------------------------------
# ENHANCED DEPLOYMENT WITH ROLLBACK
# --------------------------------------------
deploy_services() {
log "🚀 Starting deployment..."
# Set up error trap
trap 'error_exit_with_rollback "Deployment failed at step: ${BASH_COMMAND}"' ERR
# Pre-deployment checks
log "🔍 Running pre-deployment validation..."
if ! docker info >/dev/null 2>&1; then
error_exit_with_rollback "Docker daemon is not running or accessible"
fi
if ! docker compose config >/dev/null 2>&1; then
error_exit_with_rollback "Generated docker-compose.yml is invalid"
fi
# Create required directories
log "📁 Creating required directories..."
for svc in "${SERVICES[@]}"; do
mkdir -p "${APPDATA_DIR}/${svc}"
chown -R "$PUID:$PGID" "${APPDATA_DIR}/${svc}" 2>/dev/null || true
done
track_step "directories_created"
# Pull images with retry logic
log "📥 Pulling container images..."
local PULL_RETRIES=3
for attempt in $(seq 1 $PULL_RETRIES); do
if docker compose pull 2>&1 | tee -a "$LOG_FILE"; then
track_step "images_pulled"
break
elif [[ $attempt -eq $PULL_RETRIES ]]; then
error_exit_with_rollback "Failed to pull images after $PULL_RETRIES attempts"
else
log "⚠️ Pull attempt $attempt failed, retrying in 10 seconds..."
sleep 10
fi
done
# Start containers
log "🔄 Starting containers..."
if docker compose up -d 2>&1 | tee -a "$LOG_FILE"; then
track_step "containers_started"
else
log "❌ Some containers failed to start. Checking status..."
docker compose ps
error_exit_with_rollback "Container startup failed"
fi
# Clear trap on success
trap - ERR
}
# --------------------------------------------
# ENHANCED SERVICE VERIFICATION
# --------------------------------------------
verify_service_health() {
local service_name="$1"
local max_wait=300 # 5 minutes
local interval=10
log "🔍 Waiting for $service_name to be healthy..."
for ((i=0; i<max_wait; i+=interval)); do
local health=$(docker inspect --format='{{.State.Health.Status}}' "$service_name" 2>/dev/null || echo "none")
case "$health" in
"healthy")
log "✅ $service_name is healthy"
return 0
;;
"starting")
log "⏳ $service_name is starting... (${i}s elapsed)"
;;
"unhealthy")
log "❌ $service_name is unhealthy"
return 1
;;
"none")
# No health check defined, check if container is running
local state=$(docker inspect --format='{{.State.Status}}' "$service_name" 2>/dev/null || echo "unknown")
if [[ "$state" == "running" ]]; then
log "✅ $service_name is running (no health check)"
return 0
fi
;;
esac
sleep "$interval"
done
log "⚠️ $service_name health check timed out"
return 1
}
verify_services() {
log "🩺 Verifying service health..."
local FAILED_SERVICES=()
for svc in "${SERVICES[@]}"; do
if docker ps --format "{{.Names}}" | grep -qi "^${svc}$"; then
if ! verify_service_health "$svc"; then
FAILED_SERVICES+=("$svc")
fi
else
log "❌ $svc container not found"
FAILED_SERVICES+=("$svc")
fi
done
if [[ ${#FAILED_SERVICES[@]} -gt 0 ]]; then
log "⚠️ Services requiring attention:"
for svc in "${FAILED_SERVICES[@]}"; do
log " • $svc - Check logs: docker logs $svc"
done
else
log "✅ All services are healthy"
fi
}
# --------------------------------------------
# SECURITY SETUP
# --------------------------------------------
setup_security() {
log "🔒 Applying security hardening..."
# Secure sensitive files
find "$APPDATA_DIR" -name "*.env" -exec chmod 600 {} \; 2>/dev/null || true
find "$APPDATA_DIR" -name "*.key" -exec chmod 600 {} \; 2>/dev/null || true
find "$APPDATA_DIR" -name "*.pem" -exec chmod 600 {} \; 2>/dev/null || true
# Set secure permissions on homelab directory
chmod 750 "$HOME/homelab"
log "✅ Security hardening applied"
}
setup_firewall() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
info "🔥 Skipping firewall configuration on macOS (configure manually if needed)"
return 0
fi
if command -v ufw &>/dev/null; then
log "🔥 Configuring UFW firewall..."
# 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
# --------------------------------------------
validate_system_requirements
check_required_packages
collect_user_configuration
select_services
check_all_ports "${SERVICES[@]}"
# Install dependencies using abstraction
info "📦 Installing prerequisites..."
# Define packages based on OS
local required_packages
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
required_packages=("curl" "openssl" "lsof" "httpd")
else
required_packages=("ca-certificates" "curl" "gnupg" "lsb-release" "lsof" "ufw" "fail2ban" "openssl" "apache2-utils")
fi
# Install each package
for package in "${required_packages[@]}"; do
if ! command -v "${package%%-*}" &>/dev/null; then
install_package "$package"
fi
done
# Install Docker if not present
if ! check_docker_installation; then
install_docker
else
success "✅ Docker already installed and running"
fi
check_docker_compose_version
# Ensure Docker daemon is running
if ! is_service_running docker; then
start_service docker
enable_service docker
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/services" ]]; then
source "$SCRIPT_DIR/services"
local ports=$(get_service_ports "$svc")
local main_port=$(echo $ports | cut -d' ' -f1)
if [[ -n "$main_port" ]]; then
echo " • $svc: http://$(get_primary_ip):$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
}
# Execute the main installation function
install_hops