Files
hops/install
T
Stephen Klein 5affcd2e26 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>
2025-07-17 21:52:22 -04:00

935 lines
32 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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