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
+114 -23
View File
@@ -6,54 +6,85 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
HOPS (Homelab Orchestration Provisioning Script) is a comprehensive automation tool for deploying homelab infrastructure using Docker Compose. It provides menu-driven installation, management, and monitoring of popular homelab services including media servers, download clients, monitoring tools, and more. HOPS (Homelab Orchestration Provisioning Script) is a comprehensive automation tool for deploying homelab infrastructure using Docker Compose. It provides menu-driven installation, management, and monitoring of popular homelab services including media servers, download clients, monitoring tools, and more.
**Cross-Platform Support**: HOPS now supports both Linux (Ubuntu/Debian/Mint) and macOS systems with intelligent platform detection and abstraction.
## Architecture ## Architecture
### Core Components ### Core Components
- **Main Script (`hops.sh`)**: Primary entry point providing menu-driven interface for all operations - **Main Script (`hops.sh`)**: Primary entry point providing menu-driven interface for all operations
- **Installer (`hops_installer_enhanced.sh`)**: Handles service installation and Docker Compose deployment - **Installer (`install`)**: Handles service installation and Docker Compose deployment
- **Uninstaller (`hops_uninstaller_fixed.sh`)**: Manages complete removal of services and configurations - **Uninstaller (`uninstall`)**: Manages complete removal of services and configurations
- **Service Definitions (`hops_service_definitions.sh`)**: Contains Docker Compose service templates and configurations - **Service Definitions (`services`)**: Contains Docker Compose service templates and configurations
- **Library System (`lib/`)**: Modular abstraction layer for cross-platform compatibility
- `lib/common.sh`: Shared logging, UI, and utility functions
- `lib/system.sh`: OS detection, system requirements, and platform abstraction
### Key Design Patterns ### Key Design Patterns
- **Modular Architecture**: Each major function is separated into dedicated scripts - **Modular Architecture**: Each major function is separated into dedicated scripts
- **Cross-Platform Abstraction**: OS-specific operations are abstracted through lib/system.sh
- **Service-Driven**: All services are defined as Docker Compose configurations with standardized patterns - **Service-Driven**: All services are defined as Docker Compose configurations with standardized patterns
- **Error Handling**: Comprehensive error handling with logging and rollback capabilities - **Error Handling**: Comprehensive error handling with logging and rollback capabilities
- **Security First**: Built-in security hardening, firewall configuration, and secure password generation - **Security First**: Built-in security hardening, platform-appropriate firewall configuration, and secure password generation
## Development Commands ## Development Commands
### Running HOPS ### Running HOPS
```bash ```bash
# Main script (requires root) # Main script (requires root on Linux, admin on macOS)
sudo ./hops.sh sudo ./hops.sh
# Direct installation # Direct installation (Linux)
sudo ./hops_installer_enhanced.sh sudo ./install
# Direct installation (macOS)
sudo ./install
# Uninstallation # Uninstallation
sudo ./hops_uninstaller_fixed.sh sudo ./uninstall
``` ```
### Platform-Specific Requirements
**Linux (Ubuntu/Debian/Mint):**
- Root/sudo access
- Internet connection
- 2GB+ RAM, 10GB+ disk space
**macOS:**
- Admin access
- Internet connection
- 2GB+ RAM, 10GB+ disk space
- Homebrew will be installed automatically if not present
- Docker Desktop will be installed automatically via Homebrew
### Testing and Validation ### Testing and Validation
```bash ```bash
# Check script syntax # Check script syntax
bash -n hops.sh bash -n hops.sh
bash -n hops_installer_enhanced.sh bash -n install
bash -n hops_service_definitions.sh bash -n services
bash -n hops_uninstaller_fixed.sh bash -n uninstall
bash -n lib/system.sh
bash -n lib/common.sh
# Test OS detection and system requirements
source lib/common.sh && source lib/system.sh && detect_os && check_system_requirements 2 10
# Test service definitions # Test service definitions
source hops_service_definitions.sh source services
generate_service_definition jellyfin generate_service_definition jellyfin
``` ```
### Log Management ### Log Management
```bash ```bash
# View installation logs # View installation logs (Linux)
sudo tail -f /var/log/hops/hops-main-*.log sudo tail -f /var/log/hops/hops-main-*.log
# View installation logs (macOS)
sudo tail -f /usr/local/var/log/hops/hops-main-*.log
# View Docker Compose logs # View Docker Compose logs
cd ~/homelab && docker compose logs -f [service-name] cd ~/homelab && docker compose logs -f [service-name]
``` ```
@@ -63,13 +94,16 @@ cd ~/homelab && docker compose logs -f [service-name]
### Service Definition Pattern ### Service Definition Pattern
All services follow a standardized Docker Compose pattern: All services follow a standardized Docker Compose pattern:
- LinuxServer.io containers with PUID/PGID/TZ environment variables - LinuxServer.io containers with PUID/PGID/TZ environment variables
- Consistent volume mounting (`/opt/appdata` for configs, `/mnt/media` for data) - Platform-aware volume mounting:
- **Linux**: `/opt/appdata` for configs, `/mnt/media` for data
- **macOS**: `/Users/[user]/homelab/config` for configs, `/Users/[user]/homelab/media` for data
- Health checks for web services - Health checks for web services
- Unified network configuration (`homelab` network) - Unified network configuration (`homelab` network)
- Restart policy: `unless-stopped` - Restart policy: `unless-stopped`
- Platform-specific features (timezone mounts, GPU access) handled automatically
### Supported Service Categories ### Supported Service Categories
1. **Media Management**: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr, Tdarr 1. **Media Management**: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr, Tdarr, Huntarr
2. **Download Clients**: qBittorrent, Transmission, NZBGet, SABnzbd 2. **Download Clients**: qBittorrent, Transmission, NZBGet, SABnzbd
3. **Media Servers**: Jellyfin, Plex, Emby, Jellystat 3. **Media Servers**: Jellyfin, Plex, Emby, Jellystat
4. **Request Management**: Overseerr, Jellyseerr, Ombi 4. **Request Management**: Overseerr, Jellyseerr, Ombi
@@ -78,6 +112,7 @@ All services follow a standardized Docker Compose pattern:
## File Structure ## File Structure
### Linux File Structure
``` ```
~/homelab/ # Main deployment directory ~/homelab/ # Main deployment directory
├── docker-compose.yml # Generated service definitions ├── docker-compose.yml # Generated service definitions
@@ -94,6 +129,21 @@ All services follow a standardized Docker Compose pattern:
└── downloads/ └── downloads/
``` ```
### macOS File Structure
```
~/homelab/ # Main deployment directory
├── docker-compose.yml # Generated service definitions
├── .env # Environment variables
├── logs/ # Application logs
├── config/ # Application configurations
│ └── [service-name]/ # Individual service configs
└── media/ # Media storage
├── movies/
├── tv/
├── music/
└── downloads/
```
## Environment Configuration ## Environment Configuration
Key environment variables in `~/homelab/.env`: Key environment variables in `~/homelab/.env`:
@@ -105,7 +155,9 @@ Key environment variables in `~/homelab/.env`:
## Security Features ## Security Features
- **Firewall Integration**: Automatic UFW rule management - **Firewall Integration**:
- **Linux**: Automatic UFW rule management
- **macOS**: Manual firewall configuration (automatic setup skipped)
- **Secure Password Generation**: Cryptographically secure passwords - **Secure Password Generation**: Cryptographically secure passwords
- **File Permission Hardening**: Restrictive permissions on sensitive files - **File Permission Hardening**: Restrictive permissions on sensitive files
- **Network Isolation**: Docker network segregation - **Network Isolation**: Docker network segregation
@@ -113,7 +165,9 @@ Key environment variables in `~/homelab/.env`:
## Error Handling ## Error Handling
- **Comprehensive Logging**: All operations logged to `/var/log/hops/` - **Comprehensive Logging**: All operations logged to platform-specific directories
- **Linux**: `/var/log/hops/`
- **macOS**: `/usr/local/var/log/hops/`
- **Rollback Capability**: Automatic rollback on deployment failure - **Rollback Capability**: Automatic rollback on deployment failure
- **Dependency Validation**: Pre-deployment system requirement checks - **Dependency Validation**: Pre-deployment system requirement checks
- **Service Health Monitoring**: Built-in health checks for all services - **Service Health Monitoring**: Built-in health checks for all services
@@ -126,35 +180,72 @@ Key environment variables in `~/homelab/.env`:
- `show_service_status()`: Real-time monitoring - `show_service_status()`: Real-time monitoring
- `show_access_info()`: Service URL and credential display - `show_access_info()`: Service URL and credential display
### In `hops_service_definitions.sh` ### In `services`
- `generate_service_definition()`: Creates Docker Compose service blocks - `generate_service_definition()`: Creates Docker Compose service blocks
- `get_linuxserver_env()`: Standard environment variables - `get_linuxserver_env()`: Standard environment variables
- `get_web_healthcheck()`: Health check configurations - `get_web_healthcheck()`: Health check configurations
- `get_timezone_mount()`: Platform-specific timezone handling
- `get_gpu_devices()`: Platform-specific GPU access
### In `hops_installer_enhanced.sh` ### In `install`
- Service selection and dependency resolution - Service selection and dependency resolution
- Docker Compose file generation - Docker Compose file generation
- Cross-platform dependency installation
- Security hardening implementation - Security hardening implementation
- Post-deployment verification - Post-deployment verification
### In `lib/system.sh`
- `detect_os()`: Cross-platform OS detection
- `check_system_requirements()`: Platform-aware system validation
- `install_package()`: Package manager abstraction
- `install_docker()`: Platform-specific Docker installation
- `get_primary_ip()`: Network interface detection
- `get_default_*_path()`: Platform-specific path resolution
## Development Guidelines ## Development Guidelines
- **Bash Best Practices**: Use `set -e` for error handling, quote variables, use readonly for constants - **Bash Best Practices**: Use `set -e` for error handling, quote variables, use readonly for constants
- **Cross-Platform Compatibility**: Always use lib/system.sh abstraction functions instead of direct OS commands
- **Logging**: Use the logging functions (`log`, `error_exit`, `warning`, `success`, `info`) - **Logging**: Use the logging functions (`log`, `error_exit`, `warning`, `success`, `info`)
- **Color Output**: Use predefined color constants for consistent formatting - **Color Output**: Use predefined color constants for consistent formatting
- **Service Patterns**: Follow the established Docker Compose patterns when adding new services - **Service Patterns**: Follow the established Docker Compose patterns when adding new services
- **Security**: Never commit secrets, use secure password generation, implement proper file permissions - **Security**: Never commit secrets, use secure password generation, implement proper file permissions
- **Path Handling**: Use `get_default_*_path()` functions for platform-specific paths
## Common Operations ## Common Operations
### Adding New Services ### Adding New Services
1. Add service definition function in `hops_service_definitions.sh` 1. Add service definition function in `services`
2. Add service to installer menu in `hops_installer_enhanced.sh` 2. Add service to installer menu in `install`
3. Configure any required dependencies or special handling 3. Configure any required dependencies or special handling
4. Test deployment and health checks 4. Test deployment and health checks
### Debugging Issues ### Debugging Issues
1. Check logs in `/var/log/hops/` 1. Check logs in platform-specific directories:
- **Linux**: `/var/log/hops/`
- **macOS**: `/usr/local/var/log/hops/`
2. Verify Docker Compose syntax with `docker compose config` 2. Verify Docker Compose syntax with `docker compose config`
3. Check service health with `docker compose ps` 3. Check service health with `docker compose ps`
4. Review firewall rules with `sudo ufw status` 4. Review firewall rules:
- **Linux**: `sudo ufw status`
- **macOS**: Check System Preferences > Security & Privacy > Firewall
## Platform-Specific Notes
### macOS Considerations
- **Architecture Support**: Both Intel (x86_64) and Apple Silicon (ARM64) are supported
- **Docker Desktop**: Automatically installed via Homebrew if not present
- **Homebrew**: Automatically installed if not present
- **GPU Acceleration**: Not available (Docker containers cannot access macOS GPU)
- **Firewall**: Manual configuration required (automatic UFW setup skipped)
- **File Permissions**: Uses user's home directory structure for better compatibility
- **Service Management**: Uses launchctl instead of systemctl where applicable
### Linux Considerations
- **Architecture Support**: x86_64 only
- **Docker Engine**: Installed via official Docker script
- **Package Management**: Uses apt-get for Ubuntu/Debian/Mint
- **GPU Acceleration**: Available for Intel GPUs via /dev/dri passthrough
- **Firewall**: Automatic UFW configuration
- **File Permissions**: Uses system-wide directories (/opt, /mnt)
- **Service Management**: Uses systemctl for service management
+17 -16
View File
@@ -21,7 +21,7 @@
- **📖 Documentation**: Complete `CLAUDE.md` for development guidance - **📖 Documentation**: Complete `CLAUDE.md` for development guidance
### Installation Methods ### Installation Methods
- **🚀 New Secure Installer**: `sudo ./hops_install.sh` - Recommended method - **🚀 New Secure Installer**: `sudo ./setup` - Recommended method
- **⚙️ Manual Installation**: Separate privileged and user operations - **⚙️ Manual Installation**: Separate privileged and user operations
- **🔄 Legacy Support**: Original `hops.sh` still fully supported - **🔄 Legacy Support**: Original `hops.sh` still fully supported
@@ -78,6 +78,7 @@ HOPS (Homelab Orchestration Provisioning Script) automates the deployment of a c
- **Bazarr** - Subtitle management - **Bazarr** - Subtitle management
- **Prowlarr** - Indexer management - **Prowlarr** - Indexer management
- **Tdarr** - Media transcoding - **Tdarr** - Media transcoding
- **Huntarr** - Missing media discovery and automation
### ⬇️ Download Clients ### ⬇️ Download Clients
- **qBittorrent** - Feature-rich BitTorrent client - **qBittorrent** - Feature-rich BitTorrent client
@@ -131,12 +132,12 @@ chmod +x *.sh
### 2. Run Installation (New Improved Method) ### 2. Run Installation (New Improved Method)
```bash ```bash
# Option 1: Use the new secure installation wrapper # Option 1: Use the new secure installation wrapper
sudo ./hops_install.sh sudo ./setup
# Option 2: Manual two-phase installation # Option 2: Manual two-phase installation
sudo ./hops_privileged_setup.sh # Run as root sudo ./privileged-setup # Run as root
./hops_user_operations.sh generate <services> # Run as user ./user-operations generate <services> # Run as user
./hops_user_operations.sh deploy # Run as user ./user-operations deploy # Run as user
# Option 3: Legacy installation (still supported) # Option 3: Legacy installation (still supported)
sudo ./hops.sh sudo ./hops.sh
@@ -230,10 +231,10 @@ ACME_EMAIL=admin@yourdomain.com
### Service Management Commands ### Service Management Commands
```bash ```bash
# NEW: User operations script (runs without sudo) # NEW: User operations script (runs without sudo)
./hops_user_operations.sh status # View service status ./user-operations status # View service status
./hops_user_operations.sh logs <service> # View service logs ./user-operations logs <service> # View service logs
./hops_user_operations.sh deploy # Deploy services ./user-operations deploy # Deploy services
./hops_user_operations.sh stop # Stop all services ./user-operations stop # Stop all services
# Legacy: Direct Docker Compose commands # Legacy: Direct Docker Compose commands
cd ~/homelab cd ~/homelab
@@ -257,10 +258,10 @@ hops/
│ ├── validation.sh # Input validation │ ├── validation.sh # Input validation
│ ├── secrets.sh # Secret management │ ├── secrets.sh # Secret management
│ └── privileges.sh # Privilege management │ └── privileges.sh # Privilege management
├── hops_install.sh # NEW: Installation wrapper ├── setup # NEW: Installation wrapper
├── hops_privileged_setup.sh # NEW: Root-only operations ├── privileged-setup # NEW: Root-only operations
├── hops_user_operations.sh # NEW: User operations ├── user-operations # NEW: User operations
├── hops_service_definitions_improved.sh # NEW: Enhanced service definitions ├── services-improved # NEW: Enhanced service definitions
└── hops.sh # Legacy main script (still supported) └── hops.sh # Legacy main script (still supported)
``` ```
@@ -378,11 +379,11 @@ bash -n lib/*.sh
bash -n *.sh bash -n *.sh
# Test service definitions # Test service definitions
./hops_service_definitions_improved.sh list ./services-improved list
./hops_service_definitions_improved.sh generate jellyfin ./services-improved generate jellyfin
# Test new installation method # Test new installation method
sudo ./hops_install.sh sudo ./setup
# Test legacy method # Test legacy method
sudo ./hops.sh sudo ./hops.sh
-925
View File
@@ -1,925 +0,0 @@
#!/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" <<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"
echo -e "\n⬇️ DOWNLOAD CLIENTS"
echo "8) NZBGet 9) SABnzbd 10) Transmission 11) qBittorrent"
echo -e "\n🎞️ MEDIA SERVERS"
echo "12) Plex 13) Jellyfin 14) Jellystat 15) Emby"
echo -e "\n🎛️ REQUEST MANAGEMENT"
echo "16) Overseerr 17) Jellyseerr 18) Ombi"
echo -e "\n🔒 NETWORK & SECURITY"
echo "19) Traefik 20) Nginx Proxy Manager 21) Authelia"
echo -e "\n📈 MONITORING"
echo "22) Portainer 23) Watchtower 24) Uptime Kuma"
echo -e "\n📝 Select services (space-separated numbers, or 'all' for everything): "
read -a service_choices
declare -A SERVICE_MAP=(
[1]="sonarr" [2]="radarr" [3]="lidarr" [4]="readarr"
[5]="bazarr" [6]="prowlarr" [7]="tdarr" [8]="nzbget"
[9]="sabnzbd" [10]="transmission" [11]="qbittorrent"
[12]="plex" [13]="jellyfin" [14]="jellystat" [15]="emby"
[16]="overseerr" [17]="jellyseerr" [18]="ombi"
[19]="traefik" [20]="nginx-proxy-manager" [21]="authelia"
[22]="portainer" [23]="watchtower" [24]="uptime-kuma"
)
SERVICES=()
if [[ "${service_choices[0]}" == "all" ]]; then
SERVICES=($(printf '%s\n' "${SERVICE_MAP[@]}" | sort))
log "🎯 Selected all services"
else
for choice in "${service_choices[@]}"; do
[[ -n "${SERVICE_MAP[$choice]}" ]] && SERVICES+=("${SERVICE_MAP[$choice]}")
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/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
# 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/hops_service_definitions.sh" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh"
# 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/hops_service_definitions.sh"
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 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
}
File diff suppressed because it is too large Load Diff
+23 -17
View File
@@ -13,9 +13,9 @@ readonly SCRIPT_NAME="HOPS"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Default script locations # Default script locations
readonly INSTALLER_SCRIPT="$SCRIPT_DIR/hops_installer_enhanced.sh" readonly INSTALLER_SCRIPT="$SCRIPT_DIR/install"
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/hops_uninstaller_fixed.sh" readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/uninstall"
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/hops_service_definitions.sh" readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/services"
# Color codes for output # Color codes for output
readonly RED='\033[0;31m' readonly RED='\033[0;31m'
@@ -27,16 +27,16 @@ readonly CYAN='\033[0;36m'
readonly WHITE='\033[1;37m' readonly WHITE='\033[1;37m'
readonly NC='\033[0m' # No Color readonly NC='\033[0m' # No Color
# Logging setup # Load system utilities
readonly LOG_DIR="/var/log/hops" source "$SCRIPT_DIR/lib/system.sh"
readonly LOG_FILE="$LOG_DIR/hops-main-$(date +%Y%m%d-%H%M%S).log"
# Logging setup (will be set by setup_logging)
LOG_DIR=""
LOG_FILE=""
# Initialize logging # Initialize logging
init_logging() { init_logging() {
if [[ $EUID -eq 0 ]]; then setup_logging "hops-main"
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
fi
} }
# Logging function # Logging function
@@ -130,7 +130,9 @@ check_system_requirements() {
info "Checking system requirements..." info "Checking system requirements..."
# Check OS # Check OS
if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then # OS check is handled by lib/system.sh
detect_os
if [[ "$OS_NAME_LOWER" != "macos" && ! "$OS_NAME_LOWER" =~ ^(ubuntu|debian|mint)$ ]]; then
warning "This script is designed for Ubuntu/Debian/Mint systems" warning "This script is designed for Ubuntu/Debian/Mint systems"
echo -e "Continue anyway? [y/N]: " echo -e "Continue anyway? [y/N]: "
read -r continue_choice read -r continue_choice
@@ -442,7 +444,7 @@ show_access_info() {
echo -e "${BLUE}📱 Access your services at:${NC}" echo -e "${BLUE}📱 Access your services at:${NC}"
# Get local IP # Get local IP
local local_ip=$(hostname -I | awk '{print $1}') local local_ip=$(get_primary_ip)
# Service URLs with paths # Service URLs with paths
local services=( local services=(
@@ -578,14 +580,18 @@ show_help() {
echo -e " • Internet connection\n" echo -e " • Internet connection\n"
echo -e "${BLUE}📁 Default Locations:${NC}" echo -e "${BLUE}📁 Default Locations:${NC}"
echo -e " • Homelab directory: ~/homelab/" local default_homelab_path=$(get_default_homelab_path)
echo -e " • App configurations: /opt/appdata/" local default_config_path=$(get_default_config_path)
echo -e " • Media storage: /mnt/media/" local default_media_path=$(get_default_media_path)
echo -e " • Logs: /var/log/hops/\n"
echo -e " • Homelab directory: $default_homelab_path/"
echo -e " • App configurations: $default_config_path/"
echo -e " • Media storage: $default_media_path/"
echo -e " • Logs: $LOG_DIR/\n"
echo -e "${BLUE}🆘 Troubleshooting:${NC}" echo -e "${BLUE}🆘 Troubleshooting:${NC}"
echo -e " • Check logs in the 'View Logs' menu" echo -e " • Check logs in the 'View Logs' menu"
echo -e " • Verify Docker is running: systemctl status docker" echo -e " • Verify Docker is running: docker info"
echo -e " • Check container status: docker ps" echo -e " • Check container status: docker ps"
echo -e " • View service logs: docker logs [service-name]" echo -e " • View service logs: docker logs [service-name]"
echo -e " • Restart services: docker compose restart [service-name]\n" echo -e " • Restart services: docker compose restart [service-name]\n"
-679
View File
@@ -1,679 +0,0 @@
#!/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
-560
View File
@@ -1,560 +0,0 @@
#!/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
}
+123 -112
View File
@@ -11,22 +11,17 @@ install_hops() {
local SCRIPT_VERSION="3.1.0" local SCRIPT_VERSION="3.1.0"
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 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 # LOGGING SETUP
# -------------------------------------------- # --------------------------------------------
local LOG_DIR="/var/log/hops" setup_logging "homelab-setup"
local LOG_FILE="$LOG_DIR/homelab-setup-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "$LOG_DIR"
touch "$LOG_FILE"
log() { local_error_exit() {
echo -e "$(date '+%Y-%m-%d %T') - $1" | tee -a "$LOG_FILE" error_exit "$1"
}
error_exit() {
log "❌ ERROR: $1"
log "❌ Installation failed. Check logs at: $LOG_FILE"
exit 1
} }
# Enhanced error handling with rollback # Enhanced error handling with rollback
@@ -89,32 +84,20 @@ EOF
# -------------------------------------------- # --------------------------------------------
# SYSTEM REQUIREMENTS CHECK # SYSTEM REQUIREMENTS CHECK
# -------------------------------------------- # --------------------------------------------
check_system_requirements() { validate_system_requirements() {
local MIN_RAM_GB=2 local MIN_RAM_GB=2
local MIN_DISK_GB=10 local MIN_DISK_GB=10
local MIN_CORES=2 local MIN_CORES=2
log "🔍 Checking system requirements..." info "🔍 Validating system requirements..."
# Check RAM # Detect OS first
local RAM_GB=$(free -g | awk '/^Mem:/{print $2}') detect_os
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 # Check system requirements using new abstraction
local DISK_AVAIL=$(df -BG --output=avail / | tail -n 1 | tr -d 'G') check_system_requirements $MIN_RAM_GB $MIN_DISK_GB
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 return 0
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)"
} }
# -------------------------------------------- # --------------------------------------------
@@ -122,22 +105,37 @@ EOF
# -------------------------------------------- # --------------------------------------------
check_required_packages() { check_required_packages() {
local missing_packages=() local missing_packages=()
local required_packages=("curl" "wget" "openssl" "lsof" "apache2-utils") local required_packages=("curl" "wget" "openssl" "lsof")
log "📦 Checking required packages..." # 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 for package in "${required_packages[@]}"; do
if ! command -v "${package%%-*}" &>/dev/null; then 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") missing_packages+=("$package")
fi fi
done done
if [[ ${#missing_packages[@]} -gt 0 ]]; then if [[ ${#missing_packages[@]} -gt 0 ]]; then
log "📦 Installing missing packages: ${missing_packages[*]}" info "📦 Installing missing packages: ${missing_packages[*]}"
apt-get update && apt-get install -y "${missing_packages[@]}"
for package in "${missing_packages[@]}"; do
install_package "$package"
done
fi fi
log "✅ All required packages are installed" success "✅ All required packages are installed"
} }
# -------------------------------------------- # --------------------------------------------
@@ -147,24 +145,7 @@ EOF
error_exit "This script must be run as root or with sudo." error_exit "This script must be run as root or with sudo."
fi fi
# -------------------------------------------- # OS detection is handled by the lib/system.sh functions
# 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 # USER CONFIGURATION COLLECTION
@@ -201,13 +182,16 @@ EOF
# Directory configuration # Directory configuration
echo -e "\n📁 Directory Configuration" echo -e "\n📁 Directory Configuration"
echo -e "Media directory [/mnt/media]: " local default_media_path=$(get_default_media_path)
read -r media_dir local default_config_path=$(get_default_config_path)
MEDIA_DIR="${media_dir:-/mnt/media}"
echo -e "Application data directory [/opt/appdata]: " 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 read -r appdata_dir
APPDATA_DIR="${appdata_dir:-/opt/appdata}" APPDATA_DIR="${appdata_dir:-$default_config_path}"
# Create directories # Create directories
mkdir -p "$MEDIA_DIR"/{movies,tv,music,books,downloads} mkdir -p "$MEDIA_DIR"/{movies,tv,music,books,downloads}
@@ -278,8 +262,8 @@ EOF
local CONFLICTS=() local CONFLICTS=()
# Source service definitions to get port mappings # Source service definitions to get port mappings
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh" source "$SCRIPT_DIR/services"
fi fi
for svc in "${SERVICES[@]}"; do for svc in "${SERVICES[@]}"; do
@@ -438,43 +422,66 @@ EOF
select_services() { select_services() {
echo -e "\n📺 CORE MEDIA TOOLS" echo -e "\n📺 CORE MEDIA TOOLS"
echo "1) Sonarr 2) Radarr 3) Lidarr 4) Readarr" echo "1) Sonarr 2) Radarr 3) Lidarr 4) Readarr"
echo "5) Bazarr 6) Prowlarr 7) Tdarr" echo "5) Bazarr 6) Prowlarr 7) Tdarr 8) Huntarr"
echo -e "\n⬇️ DOWNLOAD CLIENTS" echo -e "\n⬇️ DOWNLOAD CLIENTS"
echo "8) NZBGet 9) SABnzbd 10) Transmission 11) qBittorrent" echo "9) NZBGet 10) SABnzbd 11) Transmission 12) qBittorrent"
echo -e "\n🎞️ MEDIA SERVERS" echo -e "\n🎞️ MEDIA SERVERS"
echo "12) Plex 13) Jellyfin 14) Jellystat 15) Emby" echo "13) Plex 14) Jellyfin 15) Jellystat 16) Emby"
echo -e "\n🎛️ REQUEST MANAGEMENT" echo -e "\n🎛️ REQUEST MANAGEMENT"
echo "16) Overseerr 17) Jellyseerr 18) Ombi" echo "17) Overseerr 18) Jellyseerr 19) Ombi"
echo -e "\n🔒 NETWORK & SECURITY" echo -e "\n🔒 NETWORK & SECURITY"
echo "19) Traefik 20) Nginx Proxy Manager 21) Authelia" echo "20) Traefik 21) Nginx Proxy Manager 22) Authelia"
echo -e "\n📈 MONITORING" echo -e "\n📈 MONITORING"
echo "22) Portainer 23) Watchtower 24) Uptime Kuma" echo "23) Portainer 24) Watchtower 25) Uptime Kuma"
echo -e "\n📝 Select services (space-separated numbers, or 'all' for everything): " echo -e "\n📝 Select services (space-separated numbers, or 'all' for everything): "
read -a service_choices read -a service_choices
declare -A SERVICE_MAP=( # Function to map service numbers to names (bash 3.2 compatible)
[1]="sonarr" [2]="radarr" [3]="lidarr" [4]="readarr" get_service_name() {
[5]="bazarr" [6]="prowlarr" [7]="tdarr" [8]="nzbget" case "$1" in
[9]="sabnzbd" [10]="transmission" [11]="qbittorrent" 1) echo "sonarr" ;;
[12]="plex" [13]="jellyfin" [14]="jellystat" [15]="emby" 2) echo "radarr" ;;
[16]="overseerr" [17]="jellyseerr" [18]="ombi" 3) echo "lidarr" ;;
[19]="traefik" [20]="nginx-proxy-manager" [21]="authelia" 4) echo "readarr" ;;
[22]="portainer" [23]="watchtower" [24]="uptime-kuma" 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=() SERVICES=()
if [[ "${service_choices[0]}" == "all" ]]; then if [[ "${service_choices[0]}" == "all" ]]; then
SERVICES=($(printf '%s\n' "${SERVICE_MAP[@]}" | sort)) 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" log "🎯 Selected all services"
else else
for choice in "${service_choices[@]}"; do for choice in "${service_choices[@]}"; do
[[ -n "${SERVICE_MAP[$choice]}" ]] && SERVICES+=("${SERVICE_MAP[$choice]}") service_name=$(get_service_name "$choice")
[[ -n "$service_name" ]] && SERVICES+=("$service_name")
done done
fi fi
@@ -494,8 +501,8 @@ EOF
# -------------------------------------------- # --------------------------------------------
check_service_dependencies() { check_service_dependencies() {
# Source service definitions for dependency resolution # Source service definitions for dependency resolution
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh" source "$SCRIPT_DIR/services"
# Resolve dependencies # Resolve dependencies
local all_services=($(resolve_dependencies "${SERVICES[@]}")) local all_services=($(resolve_dependencies "${SERVICES[@]}"))
@@ -598,8 +605,8 @@ EOF
create_env_file "$HOMELAB_DIR" create_env_file "$HOMELAB_DIR"
# Source the service definitions # Source the service definitions
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh" source "$SCRIPT_DIR/services"
# Export variables for service definitions # Export variables for service definitions
export PUID PGID TIMEZONE MEDIA_DIR APPDATA_DIR export PUID PGID TIMEZONE MEDIA_DIR APPDATA_DIR
@@ -613,7 +620,7 @@ EOF
log "✅ Generated Docker Compose with ${#SERVICES[@]} services" log "✅ Generated Docker Compose with ${#SERVICES[@]} services"
else else
error_exit "Service definitions file not found: $SCRIPT_DIR/hops_service_definitions.sh" error_exit "Service definitions file not found: $SCRIPT_DIR/services"
fi fi
# Create networks if they don't exist # Create networks if they don't exist
@@ -777,6 +784,11 @@ EOF
} }
setup_firewall() { 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 if command -v ufw &>/dev/null; then
log "🔥 Configuring UFW firewall..." log "🔥 Configuring UFW firewall..."
@@ -819,47 +831,43 @@ EOF
# -------------------------------------------- # --------------------------------------------
# MAIN INSTALLATION FLOW # MAIN INSTALLATION FLOW
# -------------------------------------------- # --------------------------------------------
check_system_requirements validate_system_requirements
detect_os
check_required_packages check_required_packages
collect_user_configuration collect_user_configuration
select_services select_services
check_all_ports "${SERVICES[@]}" check_all_ports "${SERVICES[@]}"
# Install dependencies # Install dependencies using abstraction
log "📦 Installing prerequisites..." info "📦 Installing prerequisites..."
if ! apt-get update &>/dev/null; then
error_exit "Failed to update package lists. Check your internet connection." # 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 fi
local REQUIRED_PACKAGES="ca-certificates curl gnupg lsb-release lsof ufw fail2ban openssl apache2-utils" # Install each package
if ! apt-get install -y $REQUIRED_PACKAGES 2>&1 | tee -a "$LOG_FILE"; then for package in "${required_packages[@]}"; do
error_exit "Failed to install required packages." if ! command -v "${package%%-*}" &>/dev/null; then
install_package "$package"
fi fi
done
# Install Docker if not present # Install Docker if not present
if ! command -v docker &>/dev/null; then if ! check_docker_installation; then
log "🐳 Installing Docker..." install_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 else
log "✅ Docker already installed ($(docker --version))" success "✅ Docker already installed and running"
fi fi
check_docker_compose_version check_docker_compose_version
# Ensure Docker daemon is running # Ensure Docker daemon is running
if ! systemctl is-active --quiet docker; then if ! is_service_running docker; then
log "🔄 Starting Docker daemon..." start_service docker
systemctl start docker || error_exit "Failed to start Docker daemon" enable_service docker
systemctl enable docker || log "⚠️ Could not enable Docker service"
fi fi
setup_firewall setup_firewall
@@ -891,12 +899,12 @@ EOF
echo -e "\n📱 Deployed Services:" echo -e "\n📱 Deployed Services:"
local service_count=0 local service_count=0
for svc in "${SERVICES[@]}"; do for svc in "${SERVICES[@]}"; do
if [[ -f "$SCRIPT_DIR/hops_service_definitions.sh" ]]; then if [[ -f "$SCRIPT_DIR/services" ]]; then
source "$SCRIPT_DIR/hops_service_definitions.sh" source "$SCRIPT_DIR/services"
local ports=$(get_service_ports "$svc") local ports=$(get_service_ports "$svc")
local main_port=$(echo $ports | cut -d' ' -f1) local main_port=$(echo $ports | cut -d' ' -f1)
if [[ -n "$main_port" ]]; then if [[ -n "$main_port" ]]; then
echo " • $svc: http://$(hostname -I | awk '{print $1}'):$main_port" echo " • $svc: http://$(get_primary_ip):$main_port"
((service_count++)) ((service_count++))
fi fi
fi fi
@@ -922,3 +930,6 @@ EOF
log "🎉 HOPS Enhanced deployment completed successfully!" log "🎉 HOPS Enhanced deployment completed successfully!"
return 0 return 0
} }
# Execute the main installation function
install_hops
+21 -3
View File
@@ -4,6 +4,12 @@
# Shared functions for logging, error handling, and UI # Shared functions for logging, error handling, and UI
# Version: 3.1.0 # Version: 3.1.0
# Prevent multiple sourcing
if [[ -n "${HOPS_COMMON_LOADED:-}" ]]; then
return 0
fi
readonly HOPS_COMMON_LOADED=1
# Color codes for output # Color codes for output
readonly RED='\033[0;31m' readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m' readonly GREEN='\033[0;32m'
@@ -27,7 +33,13 @@ setup_logging() {
return 1 return 1
fi fi
# Set platform-specific log directory
if [[ "$(uname -s)" == "Darwin" ]]; then
LOG_DIR="/usr/local/var/log/hops"
else
LOG_DIR="/var/log/hops" LOG_DIR="/var/log/hops"
fi
LOG_FILE="$LOG_DIR/${log_prefix}-$(date +%Y%m%d-%H%M%S).log" LOG_FILE="$LOG_DIR/${log_prefix}-$(date +%Y%m%d-%H%M%S).log"
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
@@ -214,10 +226,16 @@ is_valid_ip() {
local regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$" local regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
if [[ $ip =~ $regex ]]; then if [[ $ip =~ $regex ]]; then
local IFS='.' # Split IP into octets using parameter expansion
local -a octets=($ip) local octet1="${ip%%.*}"
local temp="${ip#*.}"
local octet2="${temp%%.*}"
temp="${temp#*.}"
local octet3="${temp%%.*}"
local octet4="${temp#*.}"
for octet in "${octets[@]}"; do # Check each octet
for octet in "$octet1" "$octet2" "$octet3" "$octet4"; do
if [[ $octet -gt 255 ]]; then if [[ $octet -gt 255 ]]; then
return 1 return 1
fi fi
+9 -9
View File
@@ -449,7 +449,7 @@ EOF
# Generate service definitions # Generate service definitions
for service in "${services[@]}"; do for service in "${services[@]}"; do
if "$SCRIPT_DIR/hops_service_definitions_improved.sh" generate "$service" >> "$compose_file"; then if "$SCRIPT_DIR/services-improved" generate "$service" >> "$compose_file"; then
success "Added service: $service" success "Added service: $service"
else else
error_exit "Failed to generate service definition for: $service" error_exit "Failed to generate service definition for: $service"
@@ -616,7 +616,7 @@ fi
# Phase 1: Privileged setup # Phase 1: Privileged setup
info "📋 Phase 1: Privileged setup (requires root)" info "📋 Phase 1: Privileged setup (requires root)"
if "$SCRIPT_DIR/hops_privileged_setup.sh"; then if "$SCRIPT_DIR/privileged-setup"; then
success "Privileged setup completed" success "Privileged setup completed"
else else
error_exit "Privileged setup failed" error_exit "Privileged setup failed"
@@ -651,7 +651,7 @@ case "$choice" in
;; ;;
4) 4)
echo "Available services:" echo "Available services:"
"$SCRIPT_DIR/hops_service_definitions_improved.sh" list "$SCRIPT_DIR/services-improved" list
read -p "Enter service names (space-separated): " -a services read -p "Enter service names (space-separated): " -a services
;; ;;
*) *)
@@ -661,10 +661,10 @@ case "$choice" in
esac esac
# Generate and deploy # Generate and deploy
if "$SCRIPT_DIR/hops_user_operations.sh" generate "${services[@]}"; then if "$SCRIPT_DIR/user-operations" generate "${services[@]}"; then
echo "Configuration generated successfully" echo "Configuration generated successfully"
if "$SCRIPT_DIR/hops_user_operations.sh" deploy; then if "$SCRIPT_DIR/user-operations" deploy; then
echo "Services deployed successfully" echo "Services deployed successfully"
else else
echo "Deployment failed" echo "Deployment failed"
@@ -677,7 +677,7 @@ fi
USERSCRIPT USERSCRIPT
success "Installation completed successfully" success "Installation completed successfully"
success "Services are now running. Check status with: ./hops_user_operations.sh status" success "Services are now running. Check status with: ./user-operations status"
EOF EOF
chmod +x "$wrapper_script" chmod +x "$wrapper_script"
@@ -703,9 +703,9 @@ main() {
;; ;;
"create-all") "create-all")
create_privileged_setup "hops_privileged_setup.sh" create_privileged_setup "privileged-setup"
create_user_script "hops_user_operations.sh" create_user_script "user-operations"
create_installation_wrapper "hops_install.sh" create_installation_wrapper "setup"
;; ;;
"run") "run")
+681 -8
View File
@@ -5,8 +5,8 @@
# Version: 3.1.0 # Version: 3.1.0
# Source common functions # Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh" source "$LIB_DIR/common.sh"
# Global variables for system info # Global variables for system info
OS_NAME="" OS_NAME=""
@@ -17,6 +17,16 @@ OS_NAME_LOWER=""
detect_os() { detect_os() {
info "🔍 Detecting operating system..." info "🔍 Detecting operating system..."
# Check if we're on macOS
if [[ "$(uname -s)" == "Darwin" ]]; then
OS_NAME="macOS"
OS_VERSION=$(sw_vers -productVersion)
OS_NAME_LOWER="macos"
success "Detected supported OS: $OS_NAME $OS_VERSION"
return 0
fi
# Linux detection
if command_exists lsb_release; then if command_exists lsb_release; then
OS_NAME=$(lsb_release -is) OS_NAME=$(lsb_release -is)
OS_VERSION=$(lsb_release -rs) OS_VERSION=$(lsb_release -rs)
@@ -35,7 +45,7 @@ detect_os() {
success "Detected supported OS: $OS_NAME $OS_VERSION" success "Detected supported OS: $OS_NAME $OS_VERSION"
;; ;;
*) *)
error_exit "Unsupported OS: $OS_NAME $OS_VERSION. Only Ubuntu/Debian/Linux Mint are supported." error_exit "Unsupported OS: $OS_NAME $OS_VERSION. Only Ubuntu/Debian/Linux Mint/macOS are supported."
;; ;;
esac esac
} }
@@ -50,13 +60,24 @@ check_system_requirements() {
# Check architecture # Check architecture
local arch=$(uname -m) local arch=$(uname -m)
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS supports both x86_64 and arm64 (Apple Silicon)
if [[ "$arch" != "x86_64" && "$arch" != "arm64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 and arm64 are supported on macOS."
fi
else
# Linux only supports x86_64
if [[ "$arch" != "x86_64" ]]; then if [[ "$arch" != "x86_64" ]]; then
error_exit "Unsupported architecture: $arch. Only x86_64 is supported." error_exit "Unsupported architecture: $arch. Only x86_64 is supported."
fi fi
fi
# Check RAM # Check RAM
local ram_gb local ram_gb
if command_exists free; then if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS memory check
ram_gb=$(sysctl -n hw.memsize | awk '{print int($1/1024/1024/1024)}')
elif command_exists free; then
ram_gb=$(free -g | awk '/^Mem:/{print $2}') ram_gb=$(free -g | awk '/^Mem:/{print $2}')
else else
ram_gb=$(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo) ram_gb=$(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo)
@@ -68,7 +89,10 @@ check_system_requirements() {
# Check disk space # Check disk space
local disk_avail_gb local disk_avail_gb
if command_exists df; then if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS disk space check
disk_avail_gb=$(df -g "$target_dir" | tail -n 1 | awk '{print $4}')
elif command_exists df; then
disk_avail_gb=$(df -BG --output=avail "$target_dir" | tail -n 1 | tr -d 'G') disk_avail_gb=$(df -BG --output=avail "$target_dir" | tail -n 1 | tr -d 'G')
else else
error_exit "Unable to check disk space - 'df' command not available" error_exit "Unable to check disk space - 'df' command not available"
@@ -79,7 +103,13 @@ check_system_requirements() {
fi fi
# Check CPU cores # Check CPU cores
local cpu_cores=$(nproc) local cpu_cores
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
cpu_cores=$(sysctl -n hw.ncpu)
else
cpu_cores=$(nproc)
fi
if [[ $cpu_cores -lt 2 ]]; then if [[ $cpu_cores -lt 2 ]]; then
warning "Only ${cpu_cores} CPU core(s) detected. 2+ cores recommended for optimal performance." warning "Only ${cpu_cores} CPU core(s) detected. 2+ cores recommended for optimal performance."
fi fi
@@ -189,7 +219,12 @@ check_sudo() {
# Get system timezone # Get system timezone
get_system_timezone() { get_system_timezone() {
if [[ -f /etc/timezone ]]; then if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS timezone detection
readlink /etc/localtime | sed 's|/var/db/timezone/zoneinfo/||' 2>/dev/null || \
ls -la /etc/localtime | awk '{print $NF}' | sed 's|/var/db/timezone/zoneinfo/||' 2>/dev/null || \
echo "UTC"
elif [[ -f /etc/timezone ]]; then
cat /etc/timezone cat /etc/timezone
elif [[ -L /etc/localtime ]]; then elif [[ -L /etc/localtime ]]; then
readlink /etc/localtime | sed 's|/usr/share/zoneinfo/||' readlink /etc/localtime | sed 's|/usr/share/zoneinfo/||'
@@ -206,9 +241,17 @@ validate_timezone() {
return 1 return 1
fi fi
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS timezone validation
if [[ -f "/var/db/timezone/zoneinfo/$timezone" ]]; then
return 0
fi
else
# Linux timezone validation
if [[ -f "/usr/share/zoneinfo/$timezone" ]]; then if [[ -f "/usr/share/zoneinfo/$timezone" ]]; then
return 0 return 0
fi fi
fi
return 1 return 1
} }
@@ -221,7 +264,12 @@ check_storage_space() {
# Create directory if it doesn't exist # Create directory if it doesn't exist
mkdir -p "$path" 2>/dev/null || true mkdir -p "$path" 2>/dev/null || true
local available_gb=$(df -BG --output=avail "$path" | tail -n 1 | tr -d 'G') local available_gb
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
available_gb=$(df -g "$path" | tail -n 1 | awk '{print $4}')
else
available_gb=$(df -BG --output=avail "$path" | tail -n 1 | tr -d 'G')
fi
if [[ $available_gb -lt $required_gb ]]; then if [[ $available_gb -lt $required_gb ]]; then
error_exit "Insufficient storage space in $path: ${available_gb}GB available, ${required_gb}GB required" error_exit "Insufficient storage space in $path: ${available_gb}GB available, ${required_gb}GB required"
@@ -273,6 +321,13 @@ get_user_info() {
check_firewall() { check_firewall() {
info "🔥 Checking firewall status..." info "🔥 Checking firewall status..."
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
# macOS uses pfctl/firewall, but we'll skip automatic configuration
warning "macOS firewall detected. Automatic firewall configuration skipped."
info "💡 You may need to manually configure firewall rules if needed."
return 0
fi
if command_exists ufw; then if command_exists ufw; then
local ufw_status=$(ufw status | head -n1 | awk '{print $2}') local ufw_status=$(ufw status | head -n1 | awk '{print $2}')
@@ -309,10 +364,628 @@ run_system_checks() {
check_system_requirements "$min_ram_gb" "$min_disk_gb" "$target_dir" check_system_requirements "$min_ram_gb" "$min_disk_gb" "$target_dir"
check_internet check_internet
check_docker_requirements check_docker_requirements
# Skip firewall check for macOS (handled differently)
if [[ "$OS_NAME_LOWER" != "macos" ]]; then
check_firewall check_firewall
fi
# Check for container environment (warning only) # Check for container environment (warning only)
check_container_environment check_container_environment
success "All system checks passed" success "All system checks passed"
} }
# Get platform-specific default paths
get_default_media_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab/media"
else
echo "/mnt/media"
fi
}
get_default_config_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab/config"
else
echo "/opt/appdata"
fi
}
get_default_homelab_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/Users/$USER/homelab"
else
echo "/home/$USER/homelab"
fi
}
# Get Docker socket path for current platform
get_docker_socket_path() {
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
echo "/var/run/docker.sock"
else
echo "/var/run/docker.sock"
fi
}
# Package management abstraction
install_package() {
local package="$1"
if [[ -z "$package" ]]; then
error_exit "install_package requires a package name"
fi
info "📦 Installing package: $package"
case "$OS_NAME_LOWER" in
"macos")
if ! command_exists brew; then
error_exit "Homebrew not found. Please install Homebrew first: https://brew.sh/"
fi
# Get the actual user (not root) to run brew commands
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
sudo -u "$actual_user" brew install "$package"
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
apt-get update && apt-get install -y "$package"
;;
*)
error_exit "Unsupported OS for package installation: $OS_NAME"
;;
esac
success "Package installed: $package"
}
# Service management abstraction
start_service() {
local service="$1"
if [[ -z "$service" ]]; then
error_exit "start_service requires a service name"
fi
info "🚀 Starting service: $service"
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
# On macOS, Docker Desktop handles this
info "Docker Desktop should be started manually or via Docker Desktop app"
else
# Use launchctl for other services
launchctl start "$service" 2>/dev/null || true
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl start "$service"
;;
*)
error_exit "Unsupported OS for service management: $OS_NAME"
;;
esac
success "Service started: $service"
}
# Check if service is running
is_service_running() {
local service="$1"
if [[ -z "$service" ]]; then
return 1
fi
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
# Check if Docker daemon is responding
docker info >/dev/null 2>&1
else
# Check with launchctl
launchctl list | grep -q "$service"
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl is-active --quiet "$service"
;;
*)
return 1
;;
esac
}
# Enable service to start on boot
enable_service() {
local service="$1"
if [[ -z "$service" ]]; then
error_exit "enable_service requires a service name"
fi
info "⚙️ Enabling service: $service"
case "$OS_NAME_LOWER" in
"macos")
if [[ "$service" == "docker" ]]; then
info "Docker Desktop auto-start should be configured in Docker Desktop settings"
else
# Use launchctl for other services
launchctl enable "$service" 2>/dev/null || true
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
systemctl enable "$service"
;;
*)
error_exit "Unsupported OS for service management: $OS_NAME"
;;
esac
success "Service enabled: $service"
}
# Get network interface IP address
get_primary_ip() {
local ip=""
case "$OS_NAME_LOWER" in
"macos")
# macOS network interface detection
ip=$(route get default | grep interface | awk '{print $2}' | head -1)
if [[ -n "$ip" ]]; then
ip=$(ifconfig "$ip" | grep 'inet ' | awk '{print $2}' | head -1)
fi
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
# Linux network interface detection
ip=$(hostname -I | awk '{print $1}')
;;
*)
# Fallback method
ip=$(ip route get 8.8.8.8 2>/dev/null | grep -oP 'src \K\S+' | head -1)
;;
esac
# Validate IP address
if is_valid_ip "$ip"; then
echo "$ip"
else
echo "localhost"
fi
}
# Remove existing Docker installation on Linux
remove_docker_linux() {
info "🗑️ Removing existing Docker installation..."
# Stop Docker service if running
if systemctl is-active --quiet docker; then
info "🛑 Stopping Docker service..."
systemctl stop docker
fi
# Stop Docker socket if running
if systemctl is-active --quiet docker.socket; then
info "🛑 Stopping Docker socket..."
systemctl stop docker.socket
fi
# Disable Docker service
if systemctl is-enabled --quiet docker; then
info "🔧 Disabling Docker service..."
systemctl disable docker
fi
# Remove Docker packages
info "🗑️ Removing Docker packages..."
apt-get remove -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || true
apt-get purge -y docker docker-engine docker.io containerd runc docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>/dev/null || true
# Remove Docker Compose standalone if installed
if [[ -f "/usr/local/bin/docker-compose" ]]; then
info "🗑️ Removing Docker Compose standalone..."
rm -f "/usr/local/bin/docker-compose"
fi
# Remove Docker data directories
info "🗑️ Removing Docker data directories..."
local docker_dirs=(
"/var/lib/docker"
"/var/lib/containerd"
"/etc/docker"
"/etc/containerd"
"/run/docker"
"/run/containerd"
"/opt/containerd"
)
for dir in "${docker_dirs[@]}"; do
if [[ -d "$dir" ]]; then
rm -rf "$dir"
fi
done
# Remove Docker group
if getent group docker >/dev/null 2>&1; then
info "🗑️ Removing Docker group..."
groupdel docker 2>/dev/null || true
fi
# Remove Docker repository
if [[ -f "/etc/apt/sources.list.d/docker.list" ]]; then
info "🗑️ Removing Docker repository..."
rm -f "/etc/apt/sources.list.d/docker.list"
fi
# Remove Docker GPG key
if [[ -f "/etc/apt/keyrings/docker.gpg" ]]; then
rm -f "/etc/apt/keyrings/docker.gpg"
fi
# Remove any remaining Docker processes
pkill -f docker 2>/dev/null || true
pkill -f containerd 2>/dev/null || true
# Clean up package manager cache
apt-get autoremove -y 2>/dev/null || true
apt-get autoclean 2>/dev/null || true
success "Docker removal completed"
}
# Remove existing Docker installation on macOS
remove_docker_macos() {
info "🗑️ Removing existing Docker installation..."
# Get the actual user (not root) for operations
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Stop Docker Desktop if running
if pgrep -f "Docker Desktop" >/dev/null 2>&1; then
info "🛑 Stopping Docker Desktop..."
sudo -u "$actual_user" osascript -e 'quit app "Docker Desktop"' 2>/dev/null || true
sleep 3
fi
# Remove Docker Desktop application
if [[ -d "/Applications/Docker.app" ]]; then
info "🗑️ Removing Docker Desktop application..."
rm -rf "/Applications/Docker.app"
fi
# Remove Docker CLI tools installed via Homebrew
if command_exists brew; then
info "🗑️ Removing Docker via Homebrew..."
sudo -u "$actual_user" brew uninstall --cask docker 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-compose 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-machine 2>/dev/null || true
sudo -u "$actual_user" brew uninstall docker-buildx 2>/dev/null || true
sudo -u "$actual_user" brew uninstall containerd 2>/dev/null || true
fi
# Remove Docker data directories
local docker_dirs=(
"/Users/$actual_user/.docker"
"/Users/$actual_user/Library/Preferences/com.docker.docker.plist"
"/Users/$actual_user/Library/Saved Application State/com.electron.docker-frontend.savedState"
"/Users/$actual_user/Library/Group Containers/group.com.docker"
"/Users/$actual_user/Library/Containers/com.docker.docker"
"/Users/$actual_user/Library/Application Support/Docker Desktop"
"/Users/$actual_user/Library/Logs/Docker Desktop"
"/Users/$actual_user/Library/Preferences/com.electron.docker-frontend.plist"
"/Users/$actual_user/Library/Caches/com.docker.docker"
)
for dir in "${docker_dirs[@]}"; do
if [[ -e "$dir" ]]; then
info "🗑️ Removing: $dir"
rm -rf "$dir"
fi
done
# Remove Docker symlinks and binaries
local docker_links=(
"/usr/local/bin/docker"
"/usr/local/bin/docker-compose"
"/usr/local/bin/docker-machine"
"/usr/local/bin/docker-buildx"
"/usr/local/bin/containerd"
"/usr/local/bin/containerd-shim"
"/usr/local/bin/containerd-shim-runc-v2"
"/usr/local/bin/ctr"
"/usr/local/bin/runc"
"/usr/local/bin/docker-credential-desktop"
"/usr/local/bin/docker-credential-ecr-login"
"/usr/local/bin/docker-credential-osxkeychain"
"/usr/local/bin/kubectl"
"/usr/local/bin/kubectl.docker"
"/usr/local/bin/vpnkit"
"/usr/local/bin/com.docker.cli"
)
for link in "${docker_links[@]}"; do
if [[ -L "$link" ]] || [[ -f "$link" ]]; then
info "🗑️ Removing: $link"
rm -f "$link"
fi
done
# Kill any remaining Docker processes
pkill -f docker 2>/dev/null || true
pkill -f com.docker 2>/dev/null || true
pkill -f containerd 2>/dev/null || true
success "Docker removal completed"
}
# Check existing Docker installation on macOS
check_existing_docker_macos() {
local docker_version=""
local docker_desktop_version=""
local installation_method=""
# Check for Docker command
if command_exists docker; then
docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
fi
# Check for Docker Desktop
if [[ -d "/Applications/Docker.app" ]]; then
docker_desktop_version=$(defaults read /Applications/Docker.app/Contents/Info.plist CFBundleShortVersionString 2>/dev/null || echo "unknown")
installation_method="Docker Desktop"
fi
# Check if installed via Homebrew
if command_exists brew; then
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
if sudo -u "$actual_user" brew list --cask docker >/dev/null 2>&1; then
installation_method="Homebrew Cask"
elif sudo -u "$actual_user" brew list docker >/dev/null 2>&1; then
installation_method="Homebrew"
fi
fi
# Return results
echo "docker_version=$docker_version"
echo "docker_desktop_version=$docker_desktop_version"
echo "installation_method=$installation_method"
}
# Install Docker for the current platform
install_docker() {
info "🐳 Installing Docker..."
case "$OS_NAME_LOWER" in
"macos")
# Check for existing Docker installation
local docker_info
docker_info=$(check_existing_docker_macos)
local docker_version=$(echo "$docker_info" | grep "docker_version=" | cut -d'=' -f2)
local docker_desktop_version=$(echo "$docker_info" | grep "docker_desktop_version=" | cut -d'=' -f2)
local installation_method=$(echo "$docker_info" | grep "installation_method=" | cut -d'=' -f2)
# If Docker is already installed, ask for confirmation to reinstall
if [[ -n "$docker_version" ]] || [[ -n "$docker_desktop_version" ]] || [[ -n "$installation_method" ]]; then
warning "Existing Docker installation detected:"
if [[ -n "$docker_version" ]]; then
info " Docker CLI version: $docker_version"
fi
if [[ -n "$docker_desktop_version" ]]; then
info " Docker Desktop version: $docker_desktop_version"
fi
if [[ -n "$installation_method" ]]; then
info " Installation method: $installation_method"
fi
echo
warning "⚠️ To ensure a clean HOPS installation, we recommend removing the existing Docker installation."
warning " This will remove all Docker data, containers, images, and volumes."
echo
read -p "❓ Do you want to remove the existing Docker installation and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_docker_macos
# Double-check removal was successful
sleep 2
if command_exists docker && docker info >/dev/null 2>&1; then
error_exit "Docker removal failed. Please manually remove Docker and try again."
fi
else
info "Keeping existing Docker installation. Checking if it's compatible..."
# Check if existing Docker is compatible
if ! docker info >/dev/null 2>&1; then
error_exit "Existing Docker installation is not running. Please start Docker Desktop manually or choose to reinstall."
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available in existing installation. Please reinstall Docker Desktop."
fi
success "Existing Docker installation is compatible"
return 0
fi
fi
# Install fresh Docker Desktop
info "📦 Installing Docker Desktop for Mac..."
# Check if Homebrew is available
if ! command_exists brew; then
warning "Homebrew not found. Installing Homebrew first..."
# Get the actual user (not root) to install Homebrew
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Install Homebrew as the actual user, not root
sudo -u "$actual_user" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Add Homebrew to PATH for current session
if [[ -f "/opt/homebrew/bin/brew" ]]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
elif [[ -f "/usr/local/bin/brew" ]]; then
eval "$(/usr/local/bin/brew shellenv)"
fi
fi
# Install Docker Desktop via Homebrew Cask
info "📦 Installing Docker Desktop via Homebrew..."
# Get the actual user (not root) to run brew commands
local actual_user
if [[ -n "$SUDO_USER" ]]; then
actual_user="$SUDO_USER"
else
actual_user="$(whoami)"
fi
# Remove conflicting compose-bridge binary if it exists
if [[ -f "/usr/local/bin/compose-bridge" ]]; then
info "🗑️ Removing conflicting compose-bridge binary..."
rm -f "/usr/local/bin/compose-bridge" 2>/dev/null || true
fi
sudo -u "$actual_user" brew install --cask docker
# Start Docker Desktop
info "🚀 Starting Docker Desktop..."
open -a Docker
# Wait for Docker to start
info "⏳ Waiting for Docker Desktop to start (this may take a few minutes)..."
local max_wait=120
local wait_time=0
while ! docker info >/dev/null 2>&1; do
if [[ $wait_time -ge $max_wait ]]; then
error_exit "Docker Desktop failed to start within $max_wait seconds. Please start it manually and try again."
fi
sleep 5
((wait_time += 5))
echo -n "."
done
echo
success "Docker Desktop installed and started successfully"
;;
"ubuntu"|"debian"|"linuxmint"|"mint")
# Check for existing Docker installation
if command_exists docker; then
local docker_version=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n1)
warning "Existing Docker installation detected:"
if [[ -n "$docker_version" ]]; then
info " Docker version: $docker_version"
fi
echo
warning "⚠️ To ensure a clean HOPS installation, we recommend removing the existing Docker installation."
warning " This will remove all Docker data, containers, images, and volumes."
echo
read -p "❓ Do you want to remove the existing Docker installation and reinstall? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
remove_docker_linux
# Double-check removal was successful
sleep 2
if command_exists docker && docker info >/dev/null 2>&1; then
error_exit "Docker removal failed. Please manually remove Docker and try again."
fi
else
info "Keeping existing Docker installation. Checking if it's compatible..."
# Check if existing Docker is compatible
if ! docker info >/dev/null 2>&1; then
error_exit "Existing Docker installation is not running. Please start Docker service manually or choose to reinstall."
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
error_exit "Docker Compose not available in existing installation. Please reinstall Docker."
fi
success "Existing Docker installation is compatible"
return 0
fi
fi
# Install fresh Docker using the official script
info "📦 Installing Docker Engine..."
curl -fsSL https://get.docker.com | sh
# Add user to docker group if we're running with sudo
if [[ -n "$SUDO_USER" ]]; then
usermod -aG docker "$SUDO_USER"
fi
# Start and enable Docker service
start_service docker
enable_service docker
success "Docker installed and configured"
;;
*)
error_exit "Unsupported OS for Docker installation: $OS_NAME"
;;
esac
}
# Check if Docker is properly installed and running
check_docker_installation() {
info "🐳 Checking Docker installation..."
# Check if Docker command exists
if ! command_exists docker; then
return 1
fi
# Check if Docker daemon is running
if ! docker info >/dev/null 2>&1; then
return 1
fi
# Check Docker Compose
if ! docker compose version >/dev/null 2>&1; then
return 1
fi
success "Docker is properly installed and running"
return 0
}
+74 -26
View File
@@ -21,6 +21,30 @@ get_linuxserver_env() {
EOF EOF
} }
# Get timezone mount path for current platform
get_timezone_mount() {
if [[ "$(uname -s)" == "Darwin" ]]; then
# macOS doesn't need timezone mount, use TZ environment variable
echo ""
else
# Linux timezone mount
echo "$(get_timezone_mount)"
fi
}
# Get GPU device access for current platform
get_gpu_devices() {
if [[ "$(uname -s)" == "Darwin" ]]; then
# macOS doesn't support GPU passthrough to Docker containers
echo ""
else
# Linux GPU device access
cat <<EOF
$(get_gpu_devices)
EOF
fi
}
# Common restart policy # Common restart policy
get_restart_policy() { get_restart_policy() {
echo " restart: unless-stopped" echo " restart: unless-stopped"
@@ -65,7 +89,7 @@ $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/sonarr:/config - \${CONFIG_ROOT}/sonarr:/config
- \${DATA_ROOT}:/data - \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 8989) $(get_web_healthcheck 8989)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -91,7 +115,7 @@ $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/radarr:/config - \${CONFIG_ROOT}/radarr:/config
- \${DATA_ROOT}:/data - \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 7878) $(get_web_healthcheck 7878)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -117,7 +141,7 @@ $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/lidarr:/config - \${CONFIG_ROOT}/lidarr:/config
- \${DATA_ROOT}:/data - \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 8686) $(get_web_healthcheck 8686)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -143,7 +167,7 @@ $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/readarr:/config - \${CONFIG_ROOT}/readarr:/config
- \${DATA_ROOT}:/data - \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 8787) $(get_web_healthcheck 8787)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -169,7 +193,7 @@ $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/bazarr:/config - \${CONFIG_ROOT}/bazarr:/config
- \${DATA_ROOT}:/data - \${DATA_ROOT}:/data
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 6767) $(get_web_healthcheck 6767)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -194,7 +218,7 @@ $(get_restart_policy)
$(get_linuxserver_env) $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/prowlarr:/config - \${CONFIG_ROOT}/prowlarr:/config
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 9696) $(get_web_healthcheck 9696)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -229,8 +253,7 @@ $(get_linuxserver_env)
- \${CONFIG_ROOT}/tdarr/logs:/app/logs - \${CONFIG_ROOT}/tdarr/logs:/app/logs
- \${DATA_ROOT}/media:/media - \${DATA_ROOT}/media:/media
- \${DATA_ROOT}/downloads/tdarr:/temp - \${DATA_ROOT}/downloads/tdarr:/temp
devices: $(get_gpu_devices)
- /dev/dri:/dev/dri # Intel GPU
$(get_web_healthcheck 8265) $(get_web_healthcheck 8265)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -243,6 +266,33 @@ $(get_homelab_network)
EOF EOF
} }
generate_huntarr() {
cat <<EOF
huntarr:
image: ghcr.io/plexguide/huntarr:latest
container_name: huntarr
$(get_restart_policy)
ports:
- "9705:9705"
environment:
$(get_linuxserver_env)
- BASE_URL=\${BASE_URL:-}
volumes:
- \${CONFIG_ROOT}/huntarr:/config
- \${DATA_ROOT}:/data
$(get_timezone_mount)
$(get_web_healthcheck 9705 "/health")
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.huntarr.rule=Host(\`huntarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.huntarr.entrypoints=websecure"
- "traefik.http.routers.huntarr.tls.certresolver=letsencrypt"
- "traefik.http.services.huntarr.loadbalancer.server.port=9705"
EOF
}
# -------------------------------------------- # --------------------------------------------
# DOWNLOAD CLIENTS # DOWNLOAD CLIENTS
# -------------------------------------------- # --------------------------------------------
@@ -263,7 +313,7 @@ $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/qbittorrent:/config - \${CONFIG_ROOT}/qbittorrent:/config
- \${DATA_ROOT}/downloads/torrents:/data/torrents - \${DATA_ROOT}/downloads/torrents:/data/torrents
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 8082) $(get_web_healthcheck 8082)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -294,7 +344,7 @@ $(get_linuxserver_env)
- \${CONFIG_ROOT}/transmission:/config - \${CONFIG_ROOT}/transmission:/config
- \${DATA_ROOT}/downloads/torrents:/data/torrents - \${DATA_ROOT}/downloads/torrents:/data/torrents
- \${DATA_ROOT}/downloads/torrents/watch:/watch - \${DATA_ROOT}/downloads/torrents/watch:/watch
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 9091 "/transmission/web/") $(get_web_healthcheck 9091 "/transmission/web/")
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -320,7 +370,7 @@ $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/nzbget:/config - \${CONFIG_ROOT}/nzbget:/config
- \${DATA_ROOT}/downloads/usenet:/data/usenet - \${DATA_ROOT}/downloads/usenet:/data/usenet
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 6789) $(get_web_healthcheck 6789)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -346,7 +396,7 @@ $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/sabnzbd:/config - \${CONFIG_ROOT}/sabnzbd:/config
- \${DATA_ROOT}/downloads/usenet:/data/usenet - \${DATA_ROOT}/downloads/usenet:/data/usenet
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 8080) $(get_web_healthcheck 8080)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -380,9 +430,8 @@ $(get_restart_policy)
- \${CONFIG_ROOT}/jellyfin:/config - \${CONFIG_ROOT}/jellyfin:/config
- \${CONFIG_ROOT}/jellyfin/cache:/cache - \${CONFIG_ROOT}/jellyfin/cache:/cache
- \${DATA_ROOT}/media:/media:ro - \${DATA_ROOT}/media:/media:ro
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
devices: $(get_gpu_devices)
- /dev/dri:/dev/dri # Intel GPU
group_add: group_add:
- "109" # render group for GPU access - "109" # render group for GPU access
$(get_web_healthcheck 8096 "/health") $(get_web_healthcheck 8096 "/health")
@@ -425,9 +474,8 @@ $(get_restart_policy)
- \${CONFIG_ROOT}/plex:/config - \${CONFIG_ROOT}/plex:/config
- \${CONFIG_ROOT}/plex/transcode:/transcode - \${CONFIG_ROOT}/plex/transcode:/transcode
- \${DATA_ROOT}/media:/data:ro - \${DATA_ROOT}/media:/data:ro
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
devices: $(get_gpu_devices)
- /dev/dri:/dev/dri # Intel GPU
$(get_web_healthcheck 32400 "/identity") $(get_web_healthcheck 32400 "/identity")
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -456,9 +504,8 @@ $(get_restart_policy)
volumes: volumes:
- \${CONFIG_ROOT}/emby:/config - \${CONFIG_ROOT}/emby:/config
- \${DATA_ROOT}/media:/data:ro - \${DATA_ROOT}/media:/data:ro
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
devices: $(get_gpu_devices)
- /dev/dri:/dev/dri # Intel GPU
$(get_web_healthcheck 8096) $(get_web_healthcheck 8096)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -538,7 +585,7 @@ $(get_restart_policy)
- TZ=\${TZ} - TZ=\${TZ}
volumes: volumes:
- \${CONFIG_ROOT}/overseerr:/app/config - \${CONFIG_ROOT}/overseerr:/app/config
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 5055) $(get_web_healthcheck 5055)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -564,7 +611,7 @@ $(get_restart_policy)
- TZ=\${TZ} - TZ=\${TZ}
volumes: volumes:
- \${CONFIG_ROOT}/jellyseerr:/app/config - \${CONFIG_ROOT}/jellyseerr:/app/config
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 5055) $(get_web_healthcheck 5055)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -589,7 +636,7 @@ $(get_restart_policy)
$(get_linuxserver_env) $(get_linuxserver_env)
volumes: volumes:
- \${CONFIG_ROOT}/ombi:/config - \${CONFIG_ROOT}/ombi:/config
- /etc/localtime:/etc/localtime:ro $(get_timezone_mount)
$(get_web_healthcheck 3579) $(get_web_healthcheck 3579)
$(get_homelab_network) $(get_homelab_network)
labels: labels:
@@ -850,6 +897,7 @@ generate_service_definition() {
"bazarr") generate_bazarr ;; "bazarr") generate_bazarr ;;
"prowlarr") generate_prowlarr ;; "prowlarr") generate_prowlarr ;;
"tdarr") generate_tdarr ;; "tdarr") generate_tdarr ;;
"huntarr") generate_huntarr ;;
# Download Clients # Download Clients
"qbittorrent") generate_qbittorrent ;; "qbittorrent") generate_qbittorrent ;;
@@ -968,7 +1016,7 @@ entryPoints:
providers: providers:
docker: docker:
endpoint: "unix:///var/run/docker.sock" endpoint: "unix://$(get_docker_socket_path)"
exposedByDefault: false exposedByDefault: false
file: file:
directory: /etc/traefik/dynamic directory: /etc/traefik/dynamic
@@ -1266,7 +1314,7 @@ show_usage() {
HOPS Service Definitions Script v3.1.0 HOPS Service Definitions Script v3.1.0
Usage: Usage:
source hops_service_definitions.sh source services
generate_hops_stack service1 service2 service3... generate_hops_stack service1 service2 service3...
Examples: Examples:
+5 -5
View File
@@ -26,7 +26,7 @@ fi
# Phase 1: Privileged setup # Phase 1: Privileged setup
info "📋 Phase 1: Privileged setup (requires root)" info "📋 Phase 1: Privileged setup (requires root)"
if "$SCRIPT_DIR/hops_privileged_setup.sh"; then if "$SCRIPT_DIR/privileged-setup"; then
success "Privileged setup completed" success "Privileged setup completed"
else else
error_exit "Privileged setup failed" error_exit "Privileged setup failed"
@@ -61,7 +61,7 @@ case "$choice" in
;; ;;
4) 4)
echo "Available services:" echo "Available services:"
"$SCRIPT_DIR/hops_service_definitions_improved.sh" list "$SCRIPT_DIR/services-improved" list
read -p "Enter service names (space-separated): " -a services read -p "Enter service names (space-separated): " -a services
;; ;;
*) *)
@@ -71,10 +71,10 @@ case "$choice" in
esac esac
# Generate and deploy # Generate and deploy
if "$SCRIPT_DIR/hops_user_operations.sh" generate "${services[@]}"; then if "$SCRIPT_DIR/user-operations" generate "${services[@]}"; then
echo "Configuration generated successfully" echo "Configuration generated successfully"
if "$SCRIPT_DIR/hops_user_operations.sh" deploy; then if "$SCRIPT_DIR/user-operations" deploy; then
echo "Services deployed successfully" echo "Services deployed successfully"
else else
echo "Deployment failed" echo "Deployment failed"
@@ -87,4 +87,4 @@ fi
USERSCRIPT USERSCRIPT
success "Installation completed successfully" success "Installation completed successfully"
success "Services are now running. Check status with: ./hops_user_operations.sh status" success "Services are now running. Check status with: ./user-operations status"
View File
+1 -1
View File
@@ -45,7 +45,7 @@ EOF
# Generate service definitions # Generate service definitions
for service in "${services[@]}"; do for service in "${services[@]}"; do
if "$SCRIPT_DIR/hops_service_definitions_improved.sh" generate "$service" >> "$compose_file"; then if "$SCRIPT_DIR/services-improved" generate "$service" >> "$compose_file"; then
success "Added service: $service" success "Added service: $service"
else else
error_exit "Failed to generate service definition for: $service" error_exit "Failed to generate service definition for: $service"