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:
@@ -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.
|
||||
|
||||
**Cross-Platform Support**: HOPS now supports both Linux (Ubuntu/Debian/Mint) and macOS systems with intelligent platform detection and abstraction.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **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
|
||||
- **Uninstaller (`hops_uninstaller_fixed.sh`)**: Manages complete removal of services and configurations
|
||||
- **Service Definitions (`hops_service_definitions.sh`)**: Contains Docker Compose service templates and configurations
|
||||
- **Installer (`install`)**: Handles service installation and Docker Compose deployment
|
||||
- **Uninstaller (`uninstall`)**: Manages complete removal of services 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
|
||||
|
||||
- **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
|
||||
- **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
|
||||
|
||||
### Running HOPS
|
||||
```bash
|
||||
# Main script (requires root)
|
||||
# Main script (requires root on Linux, admin on macOS)
|
||||
sudo ./hops.sh
|
||||
|
||||
# Direct installation
|
||||
sudo ./hops_installer_enhanced.sh
|
||||
# Direct installation (Linux)
|
||||
sudo ./install
|
||||
|
||||
# Direct installation (macOS)
|
||||
sudo ./install
|
||||
|
||||
# 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
|
||||
```bash
|
||||
# Check script syntax
|
||||
bash -n hops.sh
|
||||
bash -n hops_installer_enhanced.sh
|
||||
bash -n hops_service_definitions.sh
|
||||
bash -n hops_uninstaller_fixed.sh
|
||||
bash -n install
|
||||
bash -n services
|
||||
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
|
||||
source hops_service_definitions.sh
|
||||
source services
|
||||
generate_service_definition jellyfin
|
||||
```
|
||||
|
||||
### Log Management
|
||||
```bash
|
||||
# View installation logs
|
||||
# View installation logs (Linux)
|
||||
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
|
||||
cd ~/homelab && docker compose logs -f [service-name]
|
||||
```
|
||||
@@ -63,13 +94,16 @@ cd ~/homelab && docker compose logs -f [service-name]
|
||||
### Service Definition Pattern
|
||||
All services follow a standardized Docker Compose pattern:
|
||||
- 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
|
||||
- Unified network configuration (`homelab` network)
|
||||
- Restart policy: `unless-stopped`
|
||||
- Platform-specific features (timezone mounts, GPU access) handled automatically
|
||||
|
||||
### 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
|
||||
3. **Media Servers**: Jellyfin, Plex, Emby, Jellystat
|
||||
4. **Request Management**: Overseerr, Jellyseerr, Ombi
|
||||
@@ -78,6 +112,7 @@ All services follow a standardized Docker Compose pattern:
|
||||
|
||||
## File Structure
|
||||
|
||||
### Linux File Structure
|
||||
```
|
||||
~/homelab/ # Main deployment directory
|
||||
├── docker-compose.yml # Generated service definitions
|
||||
@@ -94,6 +129,21 @@ All services follow a standardized Docker Compose pattern:
|
||||
└── 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
|
||||
|
||||
Key environment variables in `~/homelab/.env`:
|
||||
@@ -105,7 +155,9 @@ Key environment variables in `~/homelab/.env`:
|
||||
|
||||
## 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
|
||||
- **File Permission Hardening**: Restrictive permissions on sensitive files
|
||||
- **Network Isolation**: Docker network segregation
|
||||
@@ -113,7 +165,9 @@ Key environment variables in `~/homelab/.env`:
|
||||
|
||||
## 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
|
||||
- **Dependency Validation**: Pre-deployment system requirement checks
|
||||
- **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_access_info()`: Service URL and credential display
|
||||
|
||||
### In `hops_service_definitions.sh`
|
||||
### In `services`
|
||||
- `generate_service_definition()`: Creates Docker Compose service blocks
|
||||
- `get_linuxserver_env()`: Standard environment variables
|
||||
- `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
|
||||
- Docker Compose file generation
|
||||
- Cross-platform dependency installation
|
||||
- Security hardening implementation
|
||||
- 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
|
||||
|
||||
- **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`)
|
||||
- **Color Output**: Use predefined color constants for consistent formatting
|
||||
- **Service Patterns**: Follow the established Docker Compose patterns when adding new services
|
||||
- **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
|
||||
|
||||
### Adding New Services
|
||||
1. Add service definition function in `hops_service_definitions.sh`
|
||||
2. Add service to installer menu in `hops_installer_enhanced.sh`
|
||||
1. Add service definition function in `services`
|
||||
2. Add service to installer menu in `install`
|
||||
3. Configure any required dependencies or special handling
|
||||
4. Test deployment and health checks
|
||||
|
||||
### 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`
|
||||
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
|
||||
@@ -21,7 +21,7 @@
|
||||
- **📖 Documentation**: Complete `CLAUDE.md` for development guidance
|
||||
|
||||
### 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
|
||||
- **🔄 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
|
||||
- **Prowlarr** - Indexer management
|
||||
- **Tdarr** - Media transcoding
|
||||
- **Huntarr** - Missing media discovery and automation
|
||||
|
||||
### ⬇️ Download Clients
|
||||
- **qBittorrent** - Feature-rich BitTorrent client
|
||||
@@ -131,12 +132,12 @@ chmod +x *.sh
|
||||
### 2. Run Installation (New Improved Method)
|
||||
```bash
|
||||
# Option 1: Use the new secure installation wrapper
|
||||
sudo ./hops_install.sh
|
||||
sudo ./setup
|
||||
|
||||
# Option 2: Manual two-phase installation
|
||||
sudo ./hops_privileged_setup.sh # Run as root
|
||||
./hops_user_operations.sh generate <services> # Run as user
|
||||
./hops_user_operations.sh deploy # Run as user
|
||||
sudo ./privileged-setup # Run as root
|
||||
./user-operations generate <services> # Run as user
|
||||
./user-operations deploy # Run as user
|
||||
|
||||
# Option 3: Legacy installation (still supported)
|
||||
sudo ./hops.sh
|
||||
@@ -230,10 +231,10 @@ ACME_EMAIL=admin@yourdomain.com
|
||||
### Service Management Commands
|
||||
```bash
|
||||
# NEW: User operations script (runs without sudo)
|
||||
./hops_user_operations.sh status # View service status
|
||||
./hops_user_operations.sh logs <service> # View service logs
|
||||
./hops_user_operations.sh deploy # Deploy services
|
||||
./hops_user_operations.sh stop # Stop all services
|
||||
./user-operations status # View service status
|
||||
./user-operations logs <service> # View service logs
|
||||
./user-operations deploy # Deploy services
|
||||
./user-operations stop # Stop all services
|
||||
|
||||
# Legacy: Direct Docker Compose commands
|
||||
cd ~/homelab
|
||||
@@ -257,10 +258,10 @@ hops/
|
||||
│ ├── validation.sh # Input validation
|
||||
│ ├── secrets.sh # Secret management
|
||||
│ └── privileges.sh # Privilege management
|
||||
├── hops_install.sh # NEW: Installation wrapper
|
||||
├── hops_privileged_setup.sh # NEW: Root-only operations
|
||||
├── hops_user_operations.sh # NEW: User operations
|
||||
├── hops_service_definitions_improved.sh # NEW: Enhanced service definitions
|
||||
├── setup # NEW: Installation wrapper
|
||||
├── privileged-setup # NEW: Root-only operations
|
||||
├── user-operations # NEW: User operations
|
||||
├── services-improved # NEW: Enhanced service definitions
|
||||
└── hops.sh # Legacy main script (still supported)
|
||||
```
|
||||
|
||||
@@ -378,11 +379,11 @@ bash -n lib/*.sh
|
||||
bash -n *.sh
|
||||
|
||||
# Test service definitions
|
||||
./hops_service_definitions_improved.sh list
|
||||
./hops_service_definitions_improved.sh generate jellyfin
|
||||
./services-improved list
|
||||
./services-improved generate jellyfin
|
||||
|
||||
# Test new installation method
|
||||
sudo ./hops_install.sh
|
||||
sudo ./setup
|
||||
|
||||
# Test legacy method
|
||||
sudo ./hops.sh
|
||||
|
||||
@@ -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
@@ -13,9 +13,9 @@ 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"
|
||||
readonly INSTALLER_SCRIPT="$SCRIPT_DIR/install"
|
||||
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/uninstall"
|
||||
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/services"
|
||||
|
||||
# Color codes for output
|
||||
readonly RED='\033[0;31m'
|
||||
@@ -27,16 +27,16 @@ 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"
|
||||
# Load system utilities
|
||||
source "$SCRIPT_DIR/lib/system.sh"
|
||||
|
||||
# Logging setup (will be set by setup_logging)
|
||||
LOG_DIR=""
|
||||
LOG_FILE=""
|
||||
|
||||
# Initialize logging
|
||||
init_logging() {
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$LOG_FILE"
|
||||
fi
|
||||
setup_logging "hops-main"
|
||||
}
|
||||
|
||||
# Logging function
|
||||
@@ -130,7 +130,9 @@ check_system_requirements() {
|
||||
info "Checking system requirements..."
|
||||
|
||||
# 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"
|
||||
echo -e "Continue anyway? [y/N]: "
|
||||
read -r continue_choice
|
||||
@@ -442,7 +444,7 @@ show_access_info() {
|
||||
echo -e "${BLUE}📱 Access your services at:${NC}"
|
||||
|
||||
# Get local IP
|
||||
local local_ip=$(hostname -I | awk '{print $1}')
|
||||
local local_ip=$(get_primary_ip)
|
||||
|
||||
# Service URLs with paths
|
||||
local services=(
|
||||
@@ -578,14 +580,18 @@ show_help() {
|
||||
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"
|
||||
local default_homelab_path=$(get_default_homelab_path)
|
||||
local default_config_path=$(get_default_config_path)
|
||||
local default_media_path=$(get_default_media_path)
|
||||
|
||||
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 " • 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 " • View service logs: docker logs [service-name]"
|
||||
echo -e " • Restart services: docker compose restart [service-name]\n"
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -11,22 +11,17 @@ install_hops() {
|
||||
local SCRIPT_VERSION="3.1.0"
|
||||
local SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Load system utilities
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
source "$SCRIPT_DIR/lib/system.sh"
|
||||
|
||||
# --------------------------------------------
|
||||
# LOGGING SETUP
|
||||
# --------------------------------------------
|
||||
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"
|
||||
setup_logging "homelab-setup"
|
||||
|
||||
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
|
||||
local_error_exit() {
|
||||
error_exit "$1"
|
||||
}
|
||||
|
||||
# Enhanced error handling with rollback
|
||||
@@ -89,32 +84,20 @@ EOF
|
||||
# --------------------------------------------
|
||||
# SYSTEM REQUIREMENTS CHECK
|
||||
# --------------------------------------------
|
||||
check_system_requirements() {
|
||||
validate_system_requirements() {
|
||||
local MIN_RAM_GB=2
|
||||
local MIN_DISK_GB=10
|
||||
local MIN_CORES=2
|
||||
|
||||
log "🔍 Checking system requirements..."
|
||||
info "🔍 Validating 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
|
||||
# Detect OS first
|
||||
detect_os
|
||||
|
||||
# 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 system requirements using new abstraction
|
||||
check_system_requirements $MIN_RAM_GB $MIN_DISK_GB
|
||||
|
||||
# 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)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# --------------------------------------------
|
||||
@@ -122,22 +105,37 @@ EOF
|
||||
# --------------------------------------------
|
||||
check_required_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
|
||||
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")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing_packages[@]} -gt 0 ]]; then
|
||||
log "📦 Installing missing packages: ${missing_packages[*]}"
|
||||
apt-get update && apt-get install -y "${missing_packages[@]}"
|
||||
info "📦 Installing missing packages: ${missing_packages[*]}"
|
||||
|
||||
for package in "${missing_packages[@]}"; do
|
||||
install_package "$package"
|
||||
done
|
||||
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."
|
||||
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"
|
||||
}
|
||||
# OS detection is handled by the lib/system.sh functions
|
||||
|
||||
# --------------------------------------------
|
||||
# USER CONFIGURATION COLLECTION
|
||||
@@ -201,13 +182,16 @@ EOF
|
||||
|
||||
# Directory configuration
|
||||
echo -e "\n📁 Directory Configuration"
|
||||
echo -e "Media directory [/mnt/media]: "
|
||||
read -r media_dir
|
||||
MEDIA_DIR="${media_dir:-/mnt/media}"
|
||||
local default_media_path=$(get_default_media_path)
|
||||
local default_config_path=$(get_default_config_path)
|
||||
|
||||
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
|
||||
APPDATA_DIR="${appdata_dir:-/opt/appdata}"
|
||||
APPDATA_DIR="${appdata_dir:-$default_config_path}"
|
||||
|
||||
# Create directories
|
||||
mkdir -p "$MEDIA_DIR"/{movies,tv,music,books,downloads}
|
||||
@@ -278,8 +262,8 @@ EOF
|
||||
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"
|
||||
if [[ -f "$SCRIPT_DIR/services" ]]; then
|
||||
source "$SCRIPT_DIR/services"
|
||||
fi
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
@@ -438,43 +422,66 @@ EOF
|
||||
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 "5) Bazarr 6) Prowlarr 7) Tdarr 8) Huntarr"
|
||||
|
||||
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 "12) Plex 13) Jellyfin 14) Jellystat 15) Emby"
|
||||
echo "13) Plex 14) Jellyfin 15) Jellystat 16) Emby"
|
||||
|
||||
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 "19) Traefik 20) Nginx Proxy Manager 21) Authelia"
|
||||
echo "20) Traefik 21) Nginx Proxy Manager 22) Authelia"
|
||||
|
||||
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): "
|
||||
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"
|
||||
)
|
||||
# Function to map service numbers to names (bash 3.2 compatible)
|
||||
get_service_name() {
|
||||
case "$1" in
|
||||
1) echo "sonarr" ;;
|
||||
2) echo "radarr" ;;
|
||||
3) echo "lidarr" ;;
|
||||
4) echo "readarr" ;;
|
||||
5) echo "bazarr" ;;
|
||||
6) echo "prowlarr" ;;
|
||||
7) echo "tdarr" ;;
|
||||
8) echo "huntarr" ;;
|
||||
9) echo "nzbget" ;;
|
||||
10) echo "sabnzbd" ;;
|
||||
11) echo "transmission" ;;
|
||||
12) echo "qbittorrent" ;;
|
||||
13) echo "plex" ;;
|
||||
14) echo "jellyfin" ;;
|
||||
15) echo "jellystat" ;;
|
||||
16) echo "emby" ;;
|
||||
17) echo "overseerr" ;;
|
||||
18) echo "jellyseerr" ;;
|
||||
19) echo "ombi" ;;
|
||||
20) echo "traefik" ;;
|
||||
21) echo "nginx-proxy-manager" ;;
|
||||
22) echo "authelia" ;;
|
||||
23) echo "portainer" ;;
|
||||
24) echo "watchtower" ;;
|
||||
25) echo "uptime-kuma" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
SERVICES=()
|
||||
if [[ "${service_choices[0]}" == "all" ]]; then
|
||||
SERVICES=($(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"
|
||||
else
|
||||
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
|
||||
fi
|
||||
|
||||
@@ -494,8 +501,8 @@ EOF
|
||||
# --------------------------------------------
|
||||
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"
|
||||
if [[ -f "$SCRIPT_DIR/services" ]]; then
|
||||
source "$SCRIPT_DIR/services"
|
||||
|
||||
# Resolve dependencies
|
||||
local all_services=($(resolve_dependencies "${SERVICES[@]}"))
|
||||
@@ -598,8 +605,8 @@ EOF
|
||||
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"
|
||||
if [[ -f "$SCRIPT_DIR/services" ]]; then
|
||||
source "$SCRIPT_DIR/services"
|
||||
|
||||
# Export variables for service definitions
|
||||
export PUID PGID TIMEZONE MEDIA_DIR APPDATA_DIR
|
||||
@@ -613,7 +620,7 @@ EOF
|
||||
|
||||
log "✅ Generated Docker Compose with ${#SERVICES[@]} services"
|
||||
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
|
||||
|
||||
# Create networks if they don't exist
|
||||
@@ -777,6 +784,11 @@ EOF
|
||||
}
|
||||
|
||||
setup_firewall() {
|
||||
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
|
||||
info "🔥 Skipping firewall configuration on macOS (configure manually if needed)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v ufw &>/dev/null; then
|
||||
log "🔥 Configuring UFW firewall..."
|
||||
|
||||
@@ -819,47 +831,43 @@ EOF
|
||||
# --------------------------------------------
|
||||
# MAIN INSTALLATION FLOW
|
||||
# --------------------------------------------
|
||||
check_system_requirements
|
||||
detect_os
|
||||
validate_system_requirements
|
||||
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."
|
||||
# Install dependencies using abstraction
|
||||
info "📦 Installing prerequisites..."
|
||||
|
||||
# Define packages based on OS
|
||||
local required_packages
|
||||
if [[ "$OS_NAME_LOWER" == "macos" ]]; then
|
||||
required_packages=("curl" "openssl" "lsof" "httpd")
|
||||
else
|
||||
required_packages=("ca-certificates" "curl" "gnupg" "lsb-release" "lsof" "ufw" "fail2ban" "openssl" "apache2-utils")
|
||||
fi
|
||||
|
||||
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."
|
||||
# Install each package
|
||||
for package in "${required_packages[@]}"; do
|
||||
if ! command -v "${package%%-*}" &>/dev/null; then
|
||||
install_package "$package"
|
||||
fi
|
||||
done
|
||||
|
||||
# Install Docker if not present
|
||||
if ! 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
|
||||
if ! check_docker_installation; then
|
||||
install_docker
|
||||
else
|
||||
log "✅ Docker already installed ($(docker --version))"
|
||||
success "✅ Docker already installed and running"
|
||||
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"
|
||||
if ! is_service_running docker; then
|
||||
start_service docker
|
||||
enable_service docker
|
||||
fi
|
||||
|
||||
setup_firewall
|
||||
@@ -891,12 +899,12 @@ EOF
|
||||
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"
|
||||
if [[ -f "$SCRIPT_DIR/services" ]]; then
|
||||
source "$SCRIPT_DIR/services"
|
||||
local ports=$(get_service_ports "$svc")
|
||||
local main_port=$(echo $ports | cut -d' ' -f1)
|
||||
if [[ -n "$main_port" ]]; then
|
||||
echo " • $svc: http://$(hostname -I | awk '{print $1}'):$main_port"
|
||||
echo " • $svc: http://$(get_primary_ip):$main_port"
|
||||
((service_count++))
|
||||
fi
|
||||
fi
|
||||
@@ -922,3 +930,6 @@ EOF
|
||||
log "🎉 HOPS Enhanced deployment completed successfully!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Execute the main installation function
|
||||
install_hops
|
||||
+21
-3
@@ -4,6 +4,12 @@
|
||||
# Shared functions for logging, error handling, and UI
|
||||
# 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
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
@@ -27,7 +33,13 @@ setup_logging() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Set platform-specific log directory
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
LOG_DIR="/usr/local/var/log/hops"
|
||||
else
|
||||
LOG_DIR="/var/log/hops"
|
||||
fi
|
||||
|
||||
LOG_FILE="$LOG_DIR/${log_prefix}-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
@@ -214,10 +226,16 @@ is_valid_ip() {
|
||||
local regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
|
||||
|
||||
if [[ $ip =~ $regex ]]; then
|
||||
local IFS='.'
|
||||
local -a octets=($ip)
|
||||
# Split IP into octets using parameter expansion
|
||||
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
|
||||
return 1
|
||||
fi
|
||||
|
||||
+9
-9
@@ -449,7 +449,7 @@ EOF
|
||||
|
||||
# Generate service definitions
|
||||
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"
|
||||
else
|
||||
error_exit "Failed to generate service definition for: $service"
|
||||
@@ -616,7 +616,7 @@ fi
|
||||
|
||||
# Phase 1: Privileged setup
|
||||
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"
|
||||
else
|
||||
error_exit "Privileged setup failed"
|
||||
@@ -651,7 +651,7 @@ case "$choice" in
|
||||
;;
|
||||
4)
|
||||
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
|
||||
;;
|
||||
*)
|
||||
@@ -661,10 +661,10 @@ case "$choice" in
|
||||
esac
|
||||
|
||||
# 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"
|
||||
|
||||
if "$SCRIPT_DIR/hops_user_operations.sh" deploy; then
|
||||
if "$SCRIPT_DIR/user-operations" deploy; then
|
||||
echo "Services deployed successfully"
|
||||
else
|
||||
echo "Deployment failed"
|
||||
@@ -677,7 +677,7 @@ fi
|
||||
USERSCRIPT
|
||||
|
||||
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
|
||||
|
||||
chmod +x "$wrapper_script"
|
||||
@@ -703,9 +703,9 @@ main() {
|
||||
;;
|
||||
|
||||
"create-all")
|
||||
create_privileged_setup "hops_privileged_setup.sh"
|
||||
create_user_script "hops_user_operations.sh"
|
||||
create_installation_wrapper "hops_install.sh"
|
||||
create_privileged_setup "privileged-setup"
|
||||
create_user_script "user-operations"
|
||||
create_installation_wrapper "setup"
|
||||
;;
|
||||
|
||||
"run")
|
||||
|
||||
+681
-8
@@ -5,8 +5,8 @@
|
||||
# Version: 3.1.0
|
||||
|
||||
# Source common functions
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$LIB_DIR/common.sh"
|
||||
|
||||
# Global variables for system info
|
||||
OS_NAME=""
|
||||
@@ -17,6 +17,16 @@ OS_NAME_LOWER=""
|
||||
detect_os() {
|
||||
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
|
||||
OS_NAME=$(lsb_release -is)
|
||||
OS_VERSION=$(lsb_release -rs)
|
||||
@@ -35,7 +45,7 @@ detect_os() {
|
||||
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
|
||||
}
|
||||
@@ -50,13 +60,24 @@ check_system_requirements() {
|
||||
|
||||
# Check architecture
|
||||
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
|
||||
error_exit "Unsupported architecture: $arch. Only x86_64 is supported."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check RAM
|
||||
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}')
|
||||
else
|
||||
ram_gb=$(awk '/MemTotal/ {print int($2/1024/1024)}' /proc/meminfo)
|
||||
@@ -68,7 +89,10 @@ check_system_requirements() {
|
||||
|
||||
# Check disk space
|
||||
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')
|
||||
else
|
||||
error_exit "Unable to check disk space - 'df' command not available"
|
||||
@@ -79,7 +103,13 @@ check_system_requirements() {
|
||||
fi
|
||||
|
||||
# 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
|
||||
warning "Only ${cpu_cores} CPU core(s) detected. 2+ cores recommended for optimal performance."
|
||||
fi
|
||||
@@ -189,7 +219,12 @@ check_sudo() {
|
||||
|
||||
# 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
|
||||
elif [[ -L /etc/localtime ]]; then
|
||||
readlink /etc/localtime | sed 's|/usr/share/zoneinfo/||'
|
||||
@@ -206,9 +241,17 @@ validate_timezone() {
|
||||
return 1
|
||||
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
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
@@ -221,7 +264,12 @@ check_storage_space() {
|
||||
# Create directory if it doesn't exist
|
||||
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
|
||||
error_exit "Insufficient storage space in $path: ${available_gb}GB available, ${required_gb}GB required"
|
||||
@@ -273,6 +321,13 @@ get_user_info() {
|
||||
check_firewall() {
|
||||
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
|
||||
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_internet
|
||||
check_docker_requirements
|
||||
|
||||
# Skip firewall check for macOS (handled differently)
|
||||
if [[ "$OS_NAME_LOWER" != "macos" ]]; then
|
||||
check_firewall
|
||||
fi
|
||||
|
||||
# Check for container environment (warning only)
|
||||
check_container_environment
|
||||
|
||||
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
|
||||
}
|
||||
@@ -21,6 +21,30 @@ get_linuxserver_env() {
|
||||
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
|
||||
get_restart_policy() {
|
||||
echo " restart: unless-stopped"
|
||||
@@ -65,7 +89,7 @@ $(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/sonarr:/config
|
||||
- \${DATA_ROOT}:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 8989)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -91,7 +115,7 @@ $(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/radarr:/config
|
||||
- \${DATA_ROOT}:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 7878)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -117,7 +141,7 @@ $(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/lidarr:/config
|
||||
- \${DATA_ROOT}:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 8686)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -143,7 +167,7 @@ $(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/readarr:/config
|
||||
- \${DATA_ROOT}:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 8787)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -169,7 +193,7 @@ $(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/bazarr:/config
|
||||
- \${DATA_ROOT}:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 6767)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -194,7 +218,7 @@ $(get_restart_policy)
|
||||
$(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/prowlarr:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 9696)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -229,8 +253,7 @@ $(get_linuxserver_env)
|
||||
- \${CONFIG_ROOT}/tdarr/logs:/app/logs
|
||||
- \${DATA_ROOT}/media:/media
|
||||
- \${DATA_ROOT}/downloads/tdarr:/temp
|
||||
devices:
|
||||
- /dev/dri:/dev/dri # Intel GPU
|
||||
$(get_gpu_devices)
|
||||
$(get_web_healthcheck 8265)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -243,6 +266,33 @@ $(get_homelab_network)
|
||||
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
|
||||
# --------------------------------------------
|
||||
@@ -263,7 +313,7 @@ $(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/qbittorrent:/config
|
||||
- \${DATA_ROOT}/downloads/torrents:/data/torrents
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 8082)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -294,7 +344,7 @@ $(get_linuxserver_env)
|
||||
- \${CONFIG_ROOT}/transmission:/config
|
||||
- \${DATA_ROOT}/downloads/torrents:/data/torrents
|
||||
- \${DATA_ROOT}/downloads/torrents/watch:/watch
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 9091 "/transmission/web/")
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -320,7 +370,7 @@ $(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/nzbget:/config
|
||||
- \${DATA_ROOT}/downloads/usenet:/data/usenet
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 6789)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -346,7 +396,7 @@ $(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/sabnzbd:/config
|
||||
- \${DATA_ROOT}/downloads/usenet:/data/usenet
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 8080)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -380,9 +430,8 @@ $(get_restart_policy)
|
||||
- \${CONFIG_ROOT}/jellyfin:/config
|
||||
- \${CONFIG_ROOT}/jellyfin/cache:/cache
|
||||
- \${DATA_ROOT}/media:/media:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
devices:
|
||||
- /dev/dri:/dev/dri # Intel GPU
|
||||
$(get_timezone_mount)
|
||||
$(get_gpu_devices)
|
||||
group_add:
|
||||
- "109" # render group for GPU access
|
||||
$(get_web_healthcheck 8096 "/health")
|
||||
@@ -425,9 +474,8 @@ $(get_restart_policy)
|
||||
- \${CONFIG_ROOT}/plex:/config
|
||||
- \${CONFIG_ROOT}/plex/transcode:/transcode
|
||||
- \${DATA_ROOT}/media:/data:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
devices:
|
||||
- /dev/dri:/dev/dri # Intel GPU
|
||||
$(get_timezone_mount)
|
||||
$(get_gpu_devices)
|
||||
$(get_web_healthcheck 32400 "/identity")
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -456,9 +504,8 @@ $(get_restart_policy)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/emby:/config
|
||||
- \${DATA_ROOT}/media:/data:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
devices:
|
||||
- /dev/dri:/dev/dri # Intel GPU
|
||||
$(get_timezone_mount)
|
||||
$(get_gpu_devices)
|
||||
$(get_web_healthcheck 8096)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -538,7 +585,7 @@ $(get_restart_policy)
|
||||
- TZ=\${TZ}
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/overseerr:/app/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 5055)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -564,7 +611,7 @@ $(get_restart_policy)
|
||||
- TZ=\${TZ}
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/jellyseerr:/app/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 5055)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -589,7 +636,7 @@ $(get_restart_policy)
|
||||
$(get_linuxserver_env)
|
||||
volumes:
|
||||
- \${CONFIG_ROOT}/ombi:/config
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
$(get_timezone_mount)
|
||||
$(get_web_healthcheck 3579)
|
||||
$(get_homelab_network)
|
||||
labels:
|
||||
@@ -850,6 +897,7 @@ generate_service_definition() {
|
||||
"bazarr") generate_bazarr ;;
|
||||
"prowlarr") generate_prowlarr ;;
|
||||
"tdarr") generate_tdarr ;;
|
||||
"huntarr") generate_huntarr ;;
|
||||
|
||||
# Download Clients
|
||||
"qbittorrent") generate_qbittorrent ;;
|
||||
@@ -968,7 +1016,7 @@ entryPoints:
|
||||
|
||||
providers:
|
||||
docker:
|
||||
endpoint: "unix:///var/run/docker.sock"
|
||||
endpoint: "unix://$(get_docker_socket_path)"
|
||||
exposedByDefault: false
|
||||
file:
|
||||
directory: /etc/traefik/dynamic
|
||||
@@ -1266,7 +1314,7 @@ show_usage() {
|
||||
HOPS Service Definitions Script v3.1.0
|
||||
|
||||
Usage:
|
||||
source hops_service_definitions.sh
|
||||
source services
|
||||
generate_hops_stack service1 service2 service3...
|
||||
|
||||
Examples:
|
||||
@@ -26,7 +26,7 @@ fi
|
||||
|
||||
# Phase 1: Privileged setup
|
||||
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"
|
||||
else
|
||||
error_exit "Privileged setup failed"
|
||||
@@ -61,7 +61,7 @@ case "$choice" in
|
||||
;;
|
||||
4)
|
||||
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
|
||||
;;
|
||||
*)
|
||||
@@ -71,10 +71,10 @@ case "$choice" in
|
||||
esac
|
||||
|
||||
# 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"
|
||||
|
||||
if "$SCRIPT_DIR/hops_user_operations.sh" deploy; then
|
||||
if "$SCRIPT_DIR/user-operations" deploy; then
|
||||
echo "Services deployed successfully"
|
||||
else
|
||||
echo "Deployment failed"
|
||||
@@ -87,4 +87,4 @@ fi
|
||||
USERSCRIPT
|
||||
|
||||
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"
|
||||
@@ -45,7 +45,7 @@ EOF
|
||||
|
||||
# Generate service definitions
|
||||
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"
|
||||
else
|
||||
error_exit "Failed to generate service definition for: $service"
|
||||
Reference in New Issue
Block a user