Initial release of HOPS v3.1.0
- Complete homelab orchestration and provisioning system - Support for 20+ popular homelab services - Interactive installation with dependency resolution - Security hardening and firewall configuration - Service health monitoring and management interface - Comprehensive error handling with rollback capabilities - Complete uninstaller with data preservation options 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 HOPS Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,339 @@
|
||||
# HOPS - Homelab Orchestration Provisioning Script
|
||||
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[]()
|
||||
[]()
|
||||
|
||||
**HOPS** is a comprehensive, automated deployment solution for popular homelab applications. It simplifies the process of setting up and managing Docker-based services including media servers, download clients, monitoring tools, and more.
|
||||
|
||||
## 🎯 What is HOPS?
|
||||
|
||||
HOPS (Homelab Orchestration Provisioning Script) automates the deployment of a complete homelab infrastructure using Docker Compose. It provides an intuitive menu-driven interface for selecting, configuring, and managing services with enterprise-grade features like:
|
||||
|
||||
- **Automated dependency resolution**
|
||||
- **Security hardening and firewall configuration**
|
||||
- **Service health monitoring**
|
||||
- **Rollback capabilities on failure**
|
||||
- **Comprehensive logging**
|
||||
- **User-friendly management interface**
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
### 🚀 **Easy Installation**
|
||||
- One-command installation process
|
||||
- Automatic Docker installation and configuration
|
||||
- Interactive service selection
|
||||
- Intelligent dependency resolution
|
||||
|
||||
### 🔒 **Security First**
|
||||
- Automatic firewall configuration
|
||||
- Secure password generation
|
||||
- File permission hardening
|
||||
- Network isolation
|
||||
|
||||
### 📊 **Management & Monitoring**
|
||||
- Real-time service status monitoring
|
||||
- Centralized log viewing
|
||||
- Easy service management (start/stop/restart)
|
||||
- Health checks and service verification
|
||||
|
||||
### 🔄 **Reliability**
|
||||
- Error handling with automatic rollback
|
||||
- Service dependency management
|
||||
- Port conflict detection
|
||||
- System requirements validation
|
||||
|
||||
## 📱 Supported Services
|
||||
|
||||
### 📺 Media Management (*arr Stack)
|
||||
- **Sonarr** - TV show management
|
||||
- **Radarr** - Movie management
|
||||
- **Lidarr** - Music management
|
||||
- **Readarr** - eBook/audiobook management
|
||||
- **Bazarr** - Subtitle management
|
||||
- **Prowlarr** - Indexer management
|
||||
- **Tdarr** - Media transcoding
|
||||
|
||||
### ⬇️ Download Clients
|
||||
- **qBittorrent** - Feature-rich BitTorrent client
|
||||
- **Transmission** - Lightweight BitTorrent client
|
||||
- **NZBGet** - Efficient Usenet downloader
|
||||
- **SABnzbd** - Popular Usenet client
|
||||
|
||||
### 🎞️ Media Servers
|
||||
- **Jellyfin** - Open-source media server
|
||||
- **Plex** - Popular media server platform
|
||||
- **Emby** - Feature-rich media server
|
||||
- **Jellystat** - Jellyfin statistics and monitoring
|
||||
|
||||
### 🎛️ Request Management
|
||||
- **Overseerr** - Media request management for Plex
|
||||
- **Jellyseerr** - Media request management for Jellyfin
|
||||
- **Ombi** - Media request platform
|
||||
|
||||
### 🔒 Reverse Proxy & Security
|
||||
- **Traefik** - Modern reverse proxy with automatic SSL
|
||||
- **Nginx Proxy Manager** - Easy-to-use reverse proxy
|
||||
- **Authelia** - Authentication and authorization server
|
||||
|
||||
### 📈 Monitoring & Management
|
||||
- **Portainer** - Docker container management
|
||||
- **Uptime Kuma** - Service monitoring
|
||||
- **Watchtower** - Automatic container updates
|
||||
|
||||
## 🔧 System Requirements
|
||||
|
||||
### Minimum Requirements
|
||||
- **OS**: Ubuntu 20.04+, Debian 11+, or Linux Mint 20+
|
||||
- **RAM**: 2GB (4GB+ recommended)
|
||||
- **Storage**: 10GB free space (more for media)
|
||||
- **CPU**: 2 cores recommended
|
||||
- **Network**: Internet connection required
|
||||
|
||||
### Prerequisites
|
||||
- Root/sudo access
|
||||
- x86_64 architecture
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Download HOPS
|
||||
```bash
|
||||
git clone https://github.com/yourusername/hops.git
|
||||
cd hops
|
||||
chmod +x hops.sh
|
||||
```
|
||||
|
||||
### 2. Run Installation
|
||||
```bash
|
||||
sudo ./hops.sh
|
||||
```
|
||||
|
||||
### 3. Follow the Interactive Setup
|
||||
- Select your desired services
|
||||
- Configure directories and timezone
|
||||
- Choose security options
|
||||
- Wait for automated deployment
|
||||
|
||||
### 4. Access Your Services
|
||||
The installer will provide URLs for all deployed services:
|
||||
```
|
||||
📱 Access your services at:
|
||||
● Jellyfin http://192.168.1.100:8096
|
||||
● Sonarr http://192.168.1.100:8989
|
||||
● Radarr http://192.168.1.100:7878
|
||||
● Portainer http://192.168.1.100:9000
|
||||
```
|
||||
|
||||
## 📁 Default Directory Structure
|
||||
|
||||
```
|
||||
~/homelab/ # Main homelab directory
|
||||
├── docker-compose.yml # Service definitions
|
||||
├── .env # Environment variables
|
||||
└── logs/ # Application logs
|
||||
|
||||
/opt/appdata/ # Application configurations
|
||||
├── jellyfin/
|
||||
├── sonarr/
|
||||
├── radarr/
|
||||
└── ...
|
||||
|
||||
/mnt/media/ # Media storage
|
||||
├── movies/
|
||||
├── tv/
|
||||
├── music/
|
||||
└── downloads/
|
||||
```
|
||||
|
||||
## 🎛️ Management Interface
|
||||
|
||||
HOPS includes a comprehensive management interface accessible through the main script:
|
||||
|
||||
```bash
|
||||
sudo ./hops.sh
|
||||
```
|
||||
|
||||
### Available Options:
|
||||
1. **Install HOPS** - Deploy new services
|
||||
2. **Uninstall HOPS** - Complete removal with options
|
||||
3. **Manage Services** - Start/stop/restart services
|
||||
4. **Service Status** - Real-time service monitoring
|
||||
5. **Access Information** - Get service URLs and credentials
|
||||
6. **View Logs** - Centralized log viewing
|
||||
7. **Help & Documentation** - Built-in help system
|
||||
|
||||
## 🔧 Advanced Configuration
|
||||
|
||||
### Environment Variables
|
||||
All configuration is stored in `~/homelab/.env`:
|
||||
|
||||
```bash
|
||||
# Core Configuration
|
||||
PUID=1000 # User ID
|
||||
PGID=1000 # Group ID
|
||||
TZ=America/New_York # Timezone
|
||||
|
||||
# Directory Configuration
|
||||
DATA_ROOT=/mnt/media # Media storage
|
||||
CONFIG_ROOT=/opt/appdata # App configurations
|
||||
|
||||
# Security
|
||||
DEFAULT_ADMIN_PASSWORD=... # Generated secure password
|
||||
DEFAULT_DB_PASSWORD=... # Database password
|
||||
|
||||
# Optional: Custom domain
|
||||
DOMAIN=yourdomain.com
|
||||
ACME_EMAIL=admin@yourdomain.com
|
||||
```
|
||||
|
||||
### Service Management Commands
|
||||
```bash
|
||||
# Navigate to homelab directory
|
||||
cd ~/homelab
|
||||
|
||||
# View running services
|
||||
docker compose ps
|
||||
|
||||
# View logs
|
||||
docker compose logs -f [service-name]
|
||||
|
||||
# Restart specific service
|
||||
docker compose restart [service-name]
|
||||
|
||||
# Update all services
|
||||
docker compose pull && docker compose up -d
|
||||
|
||||
# Stop all services
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
### Automatic Security Hardening
|
||||
- **Firewall Configuration**: Automatic UFW rules for service ports
|
||||
- **Secure Passwords**: Cryptographically secure password generation
|
||||
- **File Permissions**: Restrictive permissions on sensitive files
|
||||
- **Network Isolation**: Docker network segregation
|
||||
- **SSL/TLS**: Automatic certificate management with Traefik
|
||||
|
||||
### Post-Installation Security
|
||||
1. **Change Default Passwords**: Update passwords in `.env` file
|
||||
2. **Configure Reverse Proxy**: Set up Traefik or Nginx Proxy Manager
|
||||
3. **Enable Authentication**: Configure Authelia for additional security
|
||||
4. **Regular Updates**: Use Watchtower for automatic updates
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Port Conflicts
|
||||
```bash
|
||||
# Check for port conflicts
|
||||
sudo lsof -i :PORT_NUMBER
|
||||
|
||||
# View HOPS service status
|
||||
sudo ./hops.sh
|
||||
# Select option 4: Service Status
|
||||
```
|
||||
|
||||
#### Service Won't Start
|
||||
```bash
|
||||
# Check service logs
|
||||
cd ~/homelab
|
||||
docker compose logs [service-name]
|
||||
|
||||
# Restart service
|
||||
docker compose restart [service-name]
|
||||
```
|
||||
|
||||
#### Permission Issues
|
||||
```bash
|
||||
# Fix ownership of data directories
|
||||
sudo chown -R $USER:$USER /mnt/media /opt/appdata
|
||||
```
|
||||
|
||||
### Log Locations
|
||||
- **Installation Logs**: `/var/log/hops/`
|
||||
- **Service Logs**: `docker compose logs [service-name]`
|
||||
- **System Logs**: `journalctl -u docker`
|
||||
|
||||
### Getting Help
|
||||
1. Check the built-in help: `sudo ./hops.sh` → Option 7
|
||||
2. Review logs in `/var/log/hops/`
|
||||
3. Verify Docker status: `systemctl status docker`
|
||||
4. Check service health: `docker compose ps`
|
||||
|
||||
## 🔄 Backup and Recovery
|
||||
|
||||
### Backup Important Data
|
||||
```bash
|
||||
# Backup configurations
|
||||
sudo tar -czf hops-config-backup.tar.gz /opt/appdata
|
||||
|
||||
# Backup compose files
|
||||
cp ~/homelab/.env ~/homelab/docker-compose.yml /backup/location/
|
||||
```
|
||||
|
||||
### Recovery
|
||||
```bash
|
||||
# Restore configurations
|
||||
sudo tar -xzf hops-config-backup.tar.gz -C /
|
||||
|
||||
# Redeploy services
|
||||
cd ~/homelab
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 📊 Performance Tuning
|
||||
|
||||
### For Low-Resource Systems
|
||||
- Start with fewer services initially
|
||||
- Monitor resource usage with Portainer
|
||||
- Consider using lightweight alternatives (Transmission vs qBittorrent)
|
||||
|
||||
### For High-Performance Systems
|
||||
- Enable GPU transcoding in Jellyfin/Plex
|
||||
- Use SSD storage for application data
|
||||
- Configure multiple download clients for redundancy
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We welcome contributions! Please:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test thoroughly
|
||||
5. Submit a pull request
|
||||
|
||||
### Development Setup
|
||||
```bash
|
||||
git clone https://github.com/yourusername/hops.git
|
||||
cd hops
|
||||
# Make changes to scripts
|
||||
# Test with: sudo ./hops.sh
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- **LinuxServer.io** for excellent Docker images
|
||||
- **Docker** for containerization platform
|
||||
- **The Servarr Team** for the *arr applications
|
||||
- **Jellyfin Project** for the open-source media server
|
||||
- All the amazing open-source projects that make HOPS possible
|
||||
|
||||
## 📞 Support
|
||||
|
||||
- **Documentation**: Check this README and built-in help
|
||||
- **Issues**: Report bugs via GitHub Issues
|
||||
- **Community**: Join discussions in GitHub Discussions
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ for the homelab community**
|
||||
|
||||
*HOPS - Making homelab deployment simple, secure, and reliable.*
|
||||
Executable
+925
@@ -0,0 +1,925 @@
|
||||
#!/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
|
||||
}
|
||||
|
||||
Executable
+679
@@ -0,0 +1,679 @@
|
||||
#!/bin/bash
|
||||
|
||||
# HOPS - Homelab Orchestration Provisioning Script
|
||||
# Primary Management Script
|
||||
# Version: 3.1.0
|
||||
|
||||
# Exit on any error
|
||||
set -e
|
||||
|
||||
# Script version and metadata
|
||||
readonly SCRIPT_VERSION="3.1.0"
|
||||
readonly SCRIPT_NAME="HOPS"
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Default script locations
|
||||
readonly INSTALLER_SCRIPT="$SCRIPT_DIR/hops_installer_enhanced.sh"
|
||||
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/hops_uninstaller_fixed.sh"
|
||||
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/hops_service_definitions.sh"
|
||||
|
||||
# Color codes for output
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
readonly YELLOW='\033[1;33m'
|
||||
readonly BLUE='\033[0;34m'
|
||||
readonly PURPLE='\033[0;35m'
|
||||
readonly CYAN='\033[0;36m'
|
||||
readonly WHITE='\033[1;37m'
|
||||
readonly NC='\033[0m' # No Color
|
||||
|
||||
# Logging setup
|
||||
readonly LOG_DIR="/var/log/hops"
|
||||
readonly LOG_FILE="$LOG_DIR/hops-main-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
# Initialize logging
|
||||
init_logging() {
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local message="$1"
|
||||
local timestamp="$(date '+%Y-%m-%d %T')"
|
||||
|
||||
if [[ -w "$LOG_FILE" ]]; then
|
||||
echo "$timestamp - $message" >> "$LOG_FILE"
|
||||
fi
|
||||
|
||||
echo -e "$message"
|
||||
}
|
||||
|
||||
# Error handling
|
||||
error_exit() {
|
||||
log "${RED}❌ ERROR: $1${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Warning function
|
||||
warning() {
|
||||
log "${YELLOW}⚠️ WARNING: $1${NC}"
|
||||
}
|
||||
|
||||
# Success function
|
||||
success() {
|
||||
log "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
# Info function
|
||||
info() {
|
||||
log "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
# Clear screen and show header
|
||||
show_header() {
|
||||
clear
|
||||
cat << "EOF"
|
||||
|
||||
_ _ ____ ____ ____
|
||||
| | | || _ \| _ \/ ___|
|
||||
| |__| || |_) | |_) \___ \
|
||||
| __ || __/| __/ ___) |
|
||||
|_| |_||_| |_| |____/
|
||||
|
||||
EOF
|
||||
echo -e "${CYAN}🚀 Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}${NC}"
|
||||
echo -e "${WHITE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
error_exit "This script must be run as root or with sudo."
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate script dependencies
|
||||
check_dependencies() {
|
||||
local missing_deps=()
|
||||
|
||||
# Check for required scripts
|
||||
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
|
||||
missing_deps+=("Installer script: $INSTALLER_SCRIPT")
|
||||
fi
|
||||
|
||||
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
|
||||
missing_deps+=("Uninstaller script: $UNINSTALLER_SCRIPT")
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SERVICE_DEFINITIONS" ]]; then
|
||||
missing_deps+=("Service definitions: $SERVICE_DEFINITIONS")
|
||||
fi
|
||||
|
||||
# Check for required commands
|
||||
local required_commands=("curl" "wget" "systemctl")
|
||||
for cmd in "${required_commands[@]}"; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing_deps+=("Command: $cmd")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing_deps[@]} -gt 0 ]]; then
|
||||
error_exit "Missing dependencies:\n$(printf ' • %s\n' "${missing_deps[@]}")"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check system requirements
|
||||
check_system_requirements() {
|
||||
info "Checking system requirements..."
|
||||
|
||||
# Check OS
|
||||
if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then
|
||||
warning "This script is designed for Ubuntu/Debian/Mint systems"
|
||||
echo -e "Continue anyway? [y/N]: "
|
||||
read -r continue_choice
|
||||
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 0
|
||||
fi
|
||||
|
||||
# Check minimum requirements
|
||||
local ram_gb=$(free -g | awk '/^Mem:/{print $2}')
|
||||
local disk_gb=$(df -BG --output=avail / | tail -n 1 | tr -d 'G')
|
||||
|
||||
if [[ $ram_gb -lt 2 ]]; then
|
||||
warning "Low RAM detected: ${ram_gb}GB (2GB+ recommended)"
|
||||
fi
|
||||
|
||||
if [[ $disk_gb -lt 10 ]]; then
|
||||
warning "Low disk space: ${disk_gb}GB (10GB+ recommended)"
|
||||
fi
|
||||
|
||||
success "System requirements check complete"
|
||||
}
|
||||
|
||||
# Get HOPS installation status
|
||||
get_installation_status() {
|
||||
local status="not_installed"
|
||||
local homelab_dirs=(
|
||||
"$HOME/homelab"
|
||||
"/home/*/homelab"
|
||||
"/opt/homelab"
|
||||
"/srv/homelab"
|
||||
)
|
||||
|
||||
# Check for existing installation
|
||||
for dir in "${homelab_dirs[@]}"; do
|
||||
if [[ -f "$dir/docker-compose.yml" ]]; then
|
||||
status="installed"
|
||||
HOMELAB_DIR="$dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for running containers
|
||||
if command -v docker &>/dev/null && docker ps --format "{{.Names}}" | grep -qE "(sonarr|radarr|jellyfin|plex|portainer)"; then
|
||||
if [[ "$status" == "not_installed" ]]; then
|
||||
status="partial"
|
||||
else
|
||||
status="running"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$status"
|
||||
}
|
||||
|
||||
# Show installation status
|
||||
show_status() {
|
||||
local status=$(get_installation_status)
|
||||
|
||||
echo -e "${WHITE}📊 Current Status:${NC}"
|
||||
case "$status" in
|
||||
"not_installed")
|
||||
echo -e " ${RED}● Not Installed${NC}"
|
||||
;;
|
||||
"installed")
|
||||
echo -e " ${YELLOW}● Installed (stopped)${NC}"
|
||||
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
|
||||
;;
|
||||
"running")
|
||||
echo -e " ${GREEN}● Running${NC}"
|
||||
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
|
||||
;;
|
||||
"partial")
|
||||
echo -e " ${YELLOW}● Partial Installation${NC}"
|
||||
;;
|
||||
esac
|
||||
echo
|
||||
}
|
||||
|
||||
# Run installer
|
||||
run_installer() {
|
||||
info "Launching HOPS installer..."
|
||||
|
||||
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
|
||||
error_exit "Installer script not found: $INSTALLER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Source the installer function and run it
|
||||
if source "$INSTALLER_SCRIPT" && install_hops; then
|
||||
success "Installation completed successfully!"
|
||||
else
|
||||
error_exit "Installation failed. Check logs for details."
|
||||
fi
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Run uninstaller
|
||||
run_uninstaller() {
|
||||
info "Launching HOPS uninstaller..."
|
||||
|
||||
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
|
||||
error_exit "Uninstaller script not found: $UNINSTALLER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Source the uninstaller function and run it
|
||||
if source "$UNINSTALLER_SCRIPT" && uninstall_hops; then
|
||||
success "Uninstallation completed successfully!"
|
||||
else
|
||||
error_exit "Uninstallation failed. Check logs for details."
|
||||
fi
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Show service status
|
||||
show_service_status() {
|
||||
show_header
|
||||
echo -e "${WHITE}🔍 Service Status Check${NC}\n"
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
warning "Docker is not installed"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
local status=$(get_installation_status)
|
||||
if [[ "$status" == "not_installed" ]]; then
|
||||
warning "HOPS is not installed"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📊 Docker Service Status:${NC}"
|
||||
if systemctl is-active --quiet docker; then
|
||||
echo -e " ${GREEN}● Docker daemon: Running${NC}"
|
||||
else
|
||||
echo -e " ${RED}● Docker daemon: Stopped${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${BLUE}📦 Container Status:${NC}"
|
||||
|
||||
# Source service definitions to get port info
|
||||
if [[ -f "$SERVICE_DEFINITIONS" ]]; then
|
||||
source "$SERVICE_DEFINITIONS"
|
||||
fi
|
||||
|
||||
# Known HOPS services with their ports
|
||||
local services=(
|
||||
"sonarr:8989" "radarr:7878" "lidarr:8686" "readarr:8787"
|
||||
"bazarr:6767" "prowlarr:9696" "jellyfin:8096" "plex:32400"
|
||||
"overseerr:5055" "jellyseerr:5056" "portainer:9000"
|
||||
"traefik:8080" "nginx-proxy-manager:81" "qbittorrent:8082"
|
||||
"transmission:9091" "nzbget:6789" "sabnzbd:8080"
|
||||
"uptime-kuma:3001" "jellystat:3000"
|
||||
)
|
||||
|
||||
local running_count=0
|
||||
local total_count=0
|
||||
|
||||
for service_info in "${services[@]}"; do
|
||||
local service_name="${service_info%:*}"
|
||||
local service_port="${service_info#*:}"
|
||||
|
||||
if docker ps --format "{{.Names}}" | grep -q "^${service_name}$"; then
|
||||
((total_count++))
|
||||
local status_symbol="${GREEN}●${NC}"
|
||||
local status_text="Running"
|
||||
|
||||
# Check if port is accessible
|
||||
if curl -sSf --max-time 2 --connect-timeout 1 "http://localhost:${service_port}" >/dev/null 2>&1; then
|
||||
status_text="Running & Accessible"
|
||||
((running_count++))
|
||||
else
|
||||
status_text="Running (starting up)"
|
||||
status_symbol="${YELLOW}●${NC}"
|
||||
fi
|
||||
|
||||
printf " %s %-20s %s (:%s)\n" "$status_symbol" "$service_name" "$status_text" "$service_port"
|
||||
elif docker ps -a --format "{{.Names}}" | grep -q "^${service_name}$"; then
|
||||
((total_count++))
|
||||
printf " %s %-20s %s\n" "${RED}●${NC}" "$service_name" "Stopped"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $total_count -eq 0 ]]; then
|
||||
echo -e " ${YELLOW}No HOPS services found${NC}"
|
||||
else
|
||||
echo -e "\n${WHITE}📈 Summary: ${running_count}/${total_count} services running and accessible${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Manage services (start/stop/restart)
|
||||
manage_services() {
|
||||
show_header
|
||||
echo -e "${WHITE}🎛️ Service Management${NC}\n"
|
||||
|
||||
local status=$(get_installation_status)
|
||||
if [[ "$status" == "not_installed" ]]; then
|
||||
warning "HOPS is not installed"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -z "$HOMELAB_DIR" ]]; then
|
||||
error_exit "Cannot locate homelab directory with docker-compose.yml"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Available actions:${NC}"
|
||||
echo -e " 1) Start all services"
|
||||
echo -e " 2) Stop all services"
|
||||
echo -e " 3) Restart all services"
|
||||
echo -e " 4) View logs (recent)"
|
||||
echo -e " 5) View logs (follow)"
|
||||
echo -e " 6) Update services"
|
||||
echo -e " 7) Restart individual service"
|
||||
echo -e " 8) Back to main menu"
|
||||
|
||||
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
|
||||
read -r choice
|
||||
|
||||
cd "$HOMELAB_DIR"
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
info "Starting all services..."
|
||||
if docker compose up -d; then
|
||||
success "Services started"
|
||||
else
|
||||
warning "Some services may have failed to start"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
info "Stopping all services..."
|
||||
if docker compose down; then
|
||||
success "Services stopped"
|
||||
else
|
||||
warning "Some services may not have stopped cleanly"
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
info "Restarting all services..."
|
||||
if docker compose restart; then
|
||||
success "Services restarted"
|
||||
else
|
||||
warning "Some services may have failed to restart"
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
info "Showing recent logs..."
|
||||
docker compose logs --tail=100
|
||||
;;
|
||||
5)
|
||||
info "Following logs (Ctrl+C to exit)..."
|
||||
docker compose logs -f --tail=50
|
||||
;;
|
||||
6)
|
||||
info "Updating services..."
|
||||
if docker compose pull && docker compose up -d; then
|
||||
success "Services updated"
|
||||
else
|
||||
warning "Update may have failed"
|
||||
fi
|
||||
;;
|
||||
7)
|
||||
echo -e "\n${WHITE}Available services:${NC}"
|
||||
docker compose ps --services | nl -w2 -s') '
|
||||
echo -e "\n${WHITE}Enter service name to restart: ${NC}"
|
||||
read -r service_name
|
||||
if [[ -n "$service_name" ]]; then
|
||||
info "Restarting $service_name..."
|
||||
if docker compose restart "$service_name"; then
|
||||
success "$service_name restarted"
|
||||
else
|
||||
warning "Failed to restart $service_name"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
8)
|
||||
return
|
||||
;;
|
||||
*)
|
||||
warning "Invalid option"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to continue...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Show quick access URLs
|
||||
show_access_info() {
|
||||
show_header
|
||||
echo -e "${WHITE}🌐 Service Access Information${NC}\n"
|
||||
|
||||
local status=$(get_installation_status)
|
||||
if [[ "$status" == "not_installed" ]]; then
|
||||
warning "HOPS is not installed"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📱 Access your services at:${NC}"
|
||||
|
||||
# Get local IP
|
||||
local local_ip=$(hostname -I | awk '{print $1}')
|
||||
|
||||
# Service URLs with paths
|
||||
local services=(
|
||||
"Sonarr:8989:/sonarr"
|
||||
"Radarr:7878:/radarr"
|
||||
"Lidarr:8686:/lidarr"
|
||||
"Readarr:8787:/readarr"
|
||||
"Bazarr:6767:"
|
||||
"Prowlarr:9696:"
|
||||
"Jellyfin:8096:"
|
||||
"Plex:32400:/web"
|
||||
"Overseerr:5055:"
|
||||
"Jellyseerr:5056:"
|
||||
"Portainer:9000:"
|
||||
"Traefik:8080:"
|
||||
"NPM:81:"
|
||||
"qBittorrent:8082:"
|
||||
"Transmission:9091:"
|
||||
"NZBGet:6789:"
|
||||
"SABnzbd:8080:"
|
||||
"Uptime-Kuma:3001:"
|
||||
"Jellystat:3000:"
|
||||
)
|
||||
|
||||
local active_services=0
|
||||
for service_info in "${services[@]}"; do
|
||||
local service_name="${service_info%%:*}"
|
||||
local service_port="${service_info#*:}"
|
||||
local service_path="${service_port#*:}"
|
||||
service_port="${service_port%:*}"
|
||||
|
||||
if docker ps --format "{{.Names}}" | grep -qi "${service_name,,}"; then
|
||||
local url="http://${local_ip}:${service_port}${service_path}"
|
||||
printf " ${GREEN}●${NC} %-15s %s\n" "$service_name" "$url"
|
||||
((active_services++))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $active_services -eq 0 ]]; then
|
||||
echo -e " ${YELLOW}No services currently running${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${YELLOW}💡 Tips:${NC}"
|
||||
echo -e " • Bookmark these URLs for easy access"
|
||||
echo -e " • Default credentials are in the .env file"
|
||||
echo -e " • Change default passwords after first login"
|
||||
echo -e " • Some services may take a few minutes to fully start"
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Show logs
|
||||
show_logs() {
|
||||
show_header
|
||||
echo -e "${WHITE}📋 HOPS Logs${NC}\n"
|
||||
|
||||
if [[ ! -d "$LOG_DIR" ]]; then
|
||||
warning "No log directory found"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Available log files:${NC}"
|
||||
local log_files=($(find "$LOG_DIR" -name "*.log" -type f | sort -r))
|
||||
|
||||
if [[ ${#log_files[@]} -eq 0 ]]; then
|
||||
warning "No log files found"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
local count=1
|
||||
for log_file in "${log_files[@]}"; do
|
||||
local basename_log=$(basename "$log_file")
|
||||
local size=$(du -h "$log_file" | cut -f1)
|
||||
local date=$(stat -c %y "$log_file" | cut -d' ' -f1)
|
||||
|
||||
printf " %d) %-40s (%s, %s)\n" "$count" "$basename_log" "$size" "$date"
|
||||
((count++))
|
||||
done
|
||||
|
||||
echo -e "\n${WHITE}Select a log file to view [1-${#log_files[@]}] or 0 to go back: ${NC}"
|
||||
read -r choice
|
||||
|
||||
if [[ "$choice" -eq 0 ]]; then
|
||||
return
|
||||
elif [[ "$choice" -gt 0 && "$choice" -le ${#log_files[@]} ]]; then
|
||||
local selected_log="${log_files[$((choice-1))]}"
|
||||
echo -e "\n${BLUE}Showing last 50 lines of $(basename "$selected_log"):${NC}\n"
|
||||
tail -50 "$selected_log"
|
||||
else
|
||||
warning "Invalid selection"
|
||||
fi
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Show help information
|
||||
show_help() {
|
||||
show_header
|
||||
echo -e "${WHITE}📚 HOPS Help & Documentation${NC}\n"
|
||||
|
||||
echo -e "${BLUE}🎯 What is HOPS?${NC}"
|
||||
echo -e "HOPS (Homelab Orchestration Provisioning Script) is an automated installer"
|
||||
echo -e "for popular homelab applications including media servers, download clients,"
|
||||
echo -e "and monitoring tools.\n"
|
||||
|
||||
echo -e "${BLUE}🚀 Quick Start:${NC}"
|
||||
echo -e " 1. Run this script as root/sudo"
|
||||
echo -e " 2. Choose 'Install HOPS' from the menu"
|
||||
echo -e " 3. Configure directories and timezone"
|
||||
echo -e " 4. Select your desired services"
|
||||
echo -e " 5. Wait for installation to complete"
|
||||
echo -e " 6. Access services via the provided URLs\n"
|
||||
|
||||
echo -e "${BLUE}📱 Supported Services:${NC}"
|
||||
echo -e " • Media Management: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr"
|
||||
echo -e " • Download Clients: qBittorrent, Transmission, NZBGet, SABnzbd"
|
||||
echo -e " • Media Servers: Jellyfin, Plex, Emby, Jellystat"
|
||||
echo -e " • Request Management: Overseerr, Jellyseerr, Ombi"
|
||||
echo -e " • Reverse Proxy: Traefik, Nginx Proxy Manager"
|
||||
echo -e " • Monitoring: Portainer, Uptime Kuma, Watchtower\n"
|
||||
|
||||
echo -e "${BLUE}🔧 Requirements:${NC}"
|
||||
echo -e " • Ubuntu/Debian/Mint Linux"
|
||||
echo -e " • 2GB+ RAM (4GB+ recommended)"
|
||||
echo -e " • 10GB+ free disk space"
|
||||
echo -e " • Root/sudo access"
|
||||
echo -e " • Internet connection\n"
|
||||
|
||||
echo -e "${BLUE}📁 Default Locations:${NC}"
|
||||
echo -e " • Homelab directory: ~/homelab/"
|
||||
echo -e " • App configurations: /opt/appdata/"
|
||||
echo -e " • Media storage: /mnt/media/"
|
||||
echo -e " • Logs: /var/log/hops/\n"
|
||||
|
||||
echo -e "${BLUE}🆘 Troubleshooting:${NC}"
|
||||
echo -e " • Check logs in the 'View Logs' menu"
|
||||
echo -e " • Verify Docker is running: systemctl status docker"
|
||||
echo -e " • Check container status: docker ps"
|
||||
echo -e " • View service logs: docker logs [service-name]"
|
||||
echo -e " • Restart services: docker compose restart [service-name]\n"
|
||||
|
||||
echo -e "${BLUE}🔐 Security Notes:${NC}"
|
||||
echo -e " • Change default passwords in .env file after installation"
|
||||
echo -e " • Configure firewall rules as needed"
|
||||
echo -e " • Regularly update services using the management menu\n"
|
||||
|
||||
echo -e "${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Main menu
|
||||
show_main_menu() {
|
||||
local status=$(get_installation_status)
|
||||
|
||||
echo -e "${WHITE}🎛️ Main Menu:${NC}"
|
||||
echo -e " 1) Install HOPS"
|
||||
|
||||
if [[ "$status" != "not_installed" ]]; then
|
||||
echo -e " 2) Uninstall HOPS"
|
||||
echo -e " 3) Manage Services"
|
||||
echo -e " 4) Service Status"
|
||||
echo -e " 5) Access Information"
|
||||
else
|
||||
echo -e " 2) Uninstall HOPS ${YELLOW}(not installed)${NC}"
|
||||
echo -e " 3) Manage Services ${YELLOW}(not installed)${NC}"
|
||||
echo -e " 4) Service Status ${YELLOW}(not installed)${NC}"
|
||||
echo -e " 5) Access Information ${YELLOW}(not installed)${NC}"
|
||||
fi
|
||||
|
||||
echo -e " 6) View Logs"
|
||||
echo -e " 7) Help & Documentation"
|
||||
echo -e " 8) Exit"
|
||||
|
||||
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
|
||||
}
|
||||
|
||||
# Main program loop
|
||||
main() {
|
||||
init_logging
|
||||
check_root
|
||||
check_dependencies
|
||||
|
||||
while true; do
|
||||
show_header
|
||||
show_status
|
||||
show_main_menu
|
||||
|
||||
read -r choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
check_system_requirements
|
||||
run_installer
|
||||
;;
|
||||
2)
|
||||
run_uninstaller
|
||||
;;
|
||||
3)
|
||||
manage_services
|
||||
;;
|
||||
4)
|
||||
show_service_status
|
||||
;;
|
||||
5)
|
||||
show_access_info
|
||||
;;
|
||||
6)
|
||||
show_logs
|
||||
;;
|
||||
7)
|
||||
show_help
|
||||
;;
|
||||
8)
|
||||
info "Thank you for using HOPS!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
warning "Invalid option. Please select 1-8."
|
||||
sleep 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Script entry point
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
Executable
+1285
File diff suppressed because it is too large
Load Diff
Executable
+560
@@ -0,0 +1,560 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
#!/bin/bash
|
||||
|
||||
# HOPS - Homelab Orchestration Provisioning Script
|
||||
# Primary Management Script
|
||||
# Version: 3.1.0
|
||||
|
||||
# Exit on any error
|
||||
set -e
|
||||
|
||||
# Script version and metadata
|
||||
readonly SCRIPT_VERSION="3.1.0"
|
||||
readonly SCRIPT_NAME="HOPS"
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Default script locations
|
||||
readonly INSTALLER_SCRIPT="$SCRIPT_DIR/hops_installer_enhanced.sh"
|
||||
readonly UNINSTALLER_SCRIPT="$SCRIPT_DIR/hops_uninstaller_fixed.sh"
|
||||
readonly SERVICE_DEFINITIONS="$SCRIPT_DIR/hops_service_definitions.sh"
|
||||
|
||||
# Color codes for output
|
||||
readonly RED='\033[0;31m'
|
||||
readonly GREEN='\033[0;32m'
|
||||
readonly YELLOW='\033[1;33m'
|
||||
readonly BLUE='\033[0;34m'
|
||||
readonly PURPLE='\033[0;35m'
|
||||
readonly CYAN='\033[0;36m'
|
||||
readonly WHITE='\033[1;37m'
|
||||
readonly NC='\033[0m' # No Color
|
||||
|
||||
# Logging setup
|
||||
readonly LOG_DIR="/var/log/hops"
|
||||
readonly LOG_FILE="$LOG_DIR/hops-main-$(date +%Y%m%d-%H%M%S).log"
|
||||
|
||||
# Initialize logging
|
||||
init_logging() {
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$LOG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local message="$1"
|
||||
local timestamp="$(date '+%Y-%m-%d %T')"
|
||||
|
||||
if [[ -w "$LOG_FILE" ]]; then
|
||||
echo "$timestamp - $message" >> "$LOG_FILE"
|
||||
fi
|
||||
|
||||
echo -e "$message"
|
||||
}
|
||||
|
||||
# Error handling
|
||||
error_exit() {
|
||||
log "${RED}❌ ERROR: $1${NC}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Warning function
|
||||
warning() {
|
||||
log "${YELLOW}⚠️ WARNING: $1${NC}"
|
||||
}
|
||||
|
||||
# Success function
|
||||
success() {
|
||||
log "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
# Info function
|
||||
info() {
|
||||
log "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
# Clear screen and show header
|
||||
show_header() {
|
||||
clear
|
||||
cat << "EOF"
|
||||
|
||||
_ _ ____ ____ ____
|
||||
| | | || _ \| _ \/ ___|
|
||||
| |__| || |_) | |_) \___ \
|
||||
| __ || __/| __/ ___) |
|
||||
|_| |_||_| |_| |____/
|
||||
|
||||
EOF
|
||||
echo -e "${CYAN}🚀 Homelab Orchestration Provisioning Script v${SCRIPT_VERSION}${NC}"
|
||||
echo -e "${WHITE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
error_exit "This script must be run as root or with sudo."
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate script dependencies
|
||||
check_dependencies() {
|
||||
local missing_deps=()
|
||||
|
||||
# Check for required scripts
|
||||
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
|
||||
missing_deps+=("Installer script: $INSTALLER_SCRIPT")
|
||||
fi
|
||||
|
||||
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
|
||||
missing_deps+=("Uninstaller script: $UNINSTALLER_SCRIPT")
|
||||
fi
|
||||
|
||||
if [[ ! -f "$SERVICE_DEFINITIONS" ]]; then
|
||||
missing_deps+=("Service definitions: $SERVICE_DEFINITIONS")
|
||||
fi
|
||||
|
||||
# Check for required commands
|
||||
local required_commands=("curl" "wget" "systemctl")
|
||||
for cmd in "${required_commands[@]}"; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing_deps+=("Command: $cmd")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#missing_deps[@]} -gt 0 ]]; then
|
||||
error_exit "Missing dependencies:\n$(printf ' • %s\n' "${missing_deps[@]}")"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check system requirements
|
||||
check_system_requirements() {
|
||||
info "Checking system requirements..."
|
||||
|
||||
# Check OS
|
||||
if ! grep -qE '^ID=(ubuntu|debian|mint)' /etc/os-release; then
|
||||
warning "This script is designed for Ubuntu/Debian/Mint systems"
|
||||
echo -e "Continue anyway? [y/N]: "
|
||||
read -r continue_choice
|
||||
[[ ! "$continue_choice" =~ ^[Yy]$ ]] && exit 0
|
||||
fi
|
||||
|
||||
# Check minimum requirements
|
||||
local ram_gb=$(free -g | awk '/^Mem:/{print $2}')
|
||||
local disk_gb=$(df -BG --output=avail / | tail -n 1 | tr -d 'G')
|
||||
|
||||
if [[ $ram_gb -lt 2 ]]; then
|
||||
warning "Low RAM detected: ${ram_gb}GB (2GB+ recommended)"
|
||||
fi
|
||||
|
||||
if [[ $disk_gb -lt 10 ]]; then
|
||||
warning "Low disk space: ${disk_gb}GB (10GB+ recommended)"
|
||||
fi
|
||||
|
||||
success "System requirements check complete"
|
||||
}
|
||||
|
||||
# Get HOPS installation status
|
||||
get_installation_status() {
|
||||
local status="not_installed"
|
||||
local homelab_dirs=(
|
||||
"$HOME/homelab"
|
||||
"/home/*/homelab"
|
||||
"/opt/homelab"
|
||||
"/srv/homelab"
|
||||
)
|
||||
|
||||
# Check for existing installation
|
||||
for dir in "${homelab_dirs[@]}"; do
|
||||
if [[ -f "$dir/docker-compose.yml" ]]; then
|
||||
status="installed"
|
||||
HOMELAB_DIR="$dir"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# Check for running containers
|
||||
if command -v docker &>/dev/null && docker ps --format "{{.Names}}" | grep -qE "(sonarr|radarr|jellyfin|plex|portainer)"; then
|
||||
if [[ "$status" == "not_installed" ]]; then
|
||||
status="partial"
|
||||
else
|
||||
status="running"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$status"
|
||||
}
|
||||
|
||||
# Show installation status
|
||||
show_status() {
|
||||
local status=$(get_installation_status)
|
||||
|
||||
echo -e "${WHITE}📊 Current Status:${NC}"
|
||||
case "$status" in
|
||||
"not_installed")
|
||||
echo -e " ${RED}● Not Installed${NC}"
|
||||
;;
|
||||
"installed")
|
||||
echo -e " ${YELLOW}● Installed (stopped)${NC}"
|
||||
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
|
||||
;;
|
||||
"running")
|
||||
echo -e " ${GREEN}● Running${NC}"
|
||||
echo -e " ${BLUE}📂 Location: $HOMELAB_DIR${NC}"
|
||||
;;
|
||||
"partial")
|
||||
echo -e " ${YELLOW}● Partial Installation${NC}"
|
||||
;;
|
||||
esac
|
||||
echo
|
||||
}
|
||||
|
||||
# Run installer
|
||||
run_installer() {
|
||||
info "Launching HOPS installer..."
|
||||
|
||||
if [[ ! -f "$INSTALLER_SCRIPT" ]]; then
|
||||
error_exit "Installer script not found: $INSTALLER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Source the installer function and run it
|
||||
if source "$INSTALLER_SCRIPT" && install_hops; then
|
||||
success "Installation completed successfully!"
|
||||
else
|
||||
error_exit "Installation failed. Check logs for details."
|
||||
fi
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Run uninstaller
|
||||
run_uninstaller() {
|
||||
info "Launching HOPS uninstaller..."
|
||||
|
||||
if [[ ! -f "$UNINSTALLER_SCRIPT" ]]; then
|
||||
error_exit "Uninstaller script not found: $UNINSTALLER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Source the uninstaller function and run it
|
||||
if source "$UNINSTALLER_SCRIPT" && uninstall_hops; then
|
||||
success "Uninstallation completed successfully!"
|
||||
else
|
||||
error_exit "Uninstallation failed. Check logs for details."
|
||||
fi
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Show service status
|
||||
show_service_status() {
|
||||
show_header
|
||||
echo -e "${WHITE}🔍 Service Status Check${NC}\n"
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
warning "Docker is not installed"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
local status=$(get_installation_status)
|
||||
if [[ "$status" == "not_installed" ]]; then
|
||||
warning "HOPS is not installed"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📊 Docker Service Status:${NC}"
|
||||
if systemctl is-active --quiet docker; then
|
||||
echo -e " ${GREEN}● Docker daemon: Running${NC}"
|
||||
else
|
||||
echo -e " ${RED}● Docker daemon: Stopped${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${BLUE}📦 Container Status:${NC}"
|
||||
|
||||
# Source service definitions to get port info
|
||||
if [[ -f "$SERVICE_DEFINITIONS" ]]; then
|
||||
source "$SERVICE_DEFINITIONS"
|
||||
fi
|
||||
|
||||
# Known HOPS services with their ports
|
||||
local services=(
|
||||
"sonarr:8989" "radarr:7878" "lidarr:8686" "readarr:8787"
|
||||
"bazarr:6767" "prowlarr:9696" "jellyfin:8096" "plex:32400"
|
||||
"overseerr:5055" "jellyseerr:5056" "portainer:9000"
|
||||
"traefik:8080" "nginx-proxy-manager:81" "qbittorrent:8082"
|
||||
"transmission:9091" "nzbget:6789" "sabnzbd:8080"
|
||||
"uptime-kuma:3001" "jellystat:3000"
|
||||
)
|
||||
|
||||
local running_count=0
|
||||
local total_count=0
|
||||
|
||||
for service_info in "${services[@]}"; do
|
||||
local service_name="${service_info%:*}"
|
||||
local service_port="${service_info#*:}"
|
||||
|
||||
if docker ps --format "{{.Names}}" | grep -q "^${service_name}$"; then
|
||||
((total_count++))
|
||||
local status_symbol="${GREEN}●${NC}"
|
||||
local status_text="Running"
|
||||
|
||||
# Check if port is accessible
|
||||
if curl -sSf --max-time 2 --connect-timeout 1 "http://localhost:${service_port}" >/dev/null 2>&1; then
|
||||
status_text="Running & Accessible"
|
||||
((running_count++))
|
||||
else
|
||||
status_text="Running (starting up)"
|
||||
status_symbol="${YELLOW}●${NC}"
|
||||
fi
|
||||
|
||||
printf " %s %-20s %s (:%s)\n" "$status_symbol" "$service_name" "$status_text" "$service_port"
|
||||
elif docker ps -a --format "{{.Names}}" | grep -q "^${service_name}$"; then
|
||||
((total_count++))
|
||||
printf " %s %-20s %s\n" "${RED}●${NC}" "$service_name" "Stopped"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $total_count -eq 0 ]]; then
|
||||
echo -e " ${YELLOW}No HOPS services found${NC}"
|
||||
else
|
||||
echo -e "\n${WHITE}📈 Summary: ${running_count}/${total_count} services running and accessible${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Manage services (start/stop/restart)
|
||||
manage_services() {
|
||||
show_header
|
||||
echo -e "${WHITE}🎛️ Service Management${NC}\n"
|
||||
|
||||
local status=$(get_installation_status)
|
||||
if [[ "$status" == "not_installed" ]]; then
|
||||
warning "HOPS is not installed"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -z "$HOMELAB_DIR" ]]; then
|
||||
error_exit "Cannot locate homelab directory with docker-compose.yml"
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Available actions:${NC}"
|
||||
echo -e " 1) Start all services"
|
||||
echo -e " 2) Stop all services"
|
||||
echo -e " 3) Restart all services"
|
||||
echo -e " 4) View logs (recent)"
|
||||
echo -e " 5) View logs (follow)"
|
||||
echo -e " 6) Update services"
|
||||
echo -e " 7) Restart individual service"
|
||||
echo -e " 8) Back to main menu"
|
||||
|
||||
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
|
||||
read -r choice
|
||||
|
||||
cd "$HOMELAB_DIR"
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
info "Starting all services..."
|
||||
if docker compose up -d; then
|
||||
success "Services started"
|
||||
else
|
||||
warning "Some services may have failed to start"
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
info "Stopping all services..."
|
||||
if docker compose down; then
|
||||
success "Services stopped"
|
||||
else
|
||||
warning "Some services may not have stopped cleanly"
|
||||
fi
|
||||
;;
|
||||
3)
|
||||
info "Restarting all services..."
|
||||
if docker compose restart; then
|
||||
success "Services restarted"
|
||||
else
|
||||
warning "Some services may have failed to restart"
|
||||
fi
|
||||
;;
|
||||
4)
|
||||
info "Showing recent logs..."
|
||||
docker compose logs --tail=100
|
||||
;;
|
||||
5)
|
||||
info "Following logs (Ctrl+C to exit)..."
|
||||
docker compose logs -f --tail=50
|
||||
;;
|
||||
6)
|
||||
info "Updating services..."
|
||||
if docker compose pull && docker compose up -d; then
|
||||
success "Services updated"
|
||||
else
|
||||
warning "Update may have failed"
|
||||
fi
|
||||
;;
|
||||
7)
|
||||
echo -e "\n${WHITE}Available services:${NC}"
|
||||
docker compose ps --services | nl -w2 -s') '
|
||||
echo -e "\n${WHITE}Enter service name to restart: ${NC}"
|
||||
read -r service_name
|
||||
if [[ -n "$service_name" ]]; then
|
||||
info "Restarting $service_name..."
|
||||
if docker compose restart "$service_name"; then
|
||||
success "$service_name restarted"
|
||||
else
|
||||
warning "Failed to restart $service_name"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
8)
|
||||
return
|
||||
;;
|
||||
*)
|
||||
warning "Invalid option"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to continue...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Show quick access URLs
|
||||
show_access_info() {
|
||||
show_header
|
||||
echo -e "${WHITE}🌐 Service Access Information${NC}\n"
|
||||
|
||||
local status=$(get_installation_status)
|
||||
if [[ "$status" == "not_installed" ]]; then
|
||||
warning "HOPS is not installed"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📱 Access your services at:${NC}"
|
||||
|
||||
# Get local IP
|
||||
local local_ip=$(hostname -I | awk '{print $1}')
|
||||
|
||||
# Service URLs with paths
|
||||
local services=(
|
||||
"Sonarr:8989:/sonarr"
|
||||
"Radarr:7878:/radarr"
|
||||
"Lidarr:8686:/lidarr"
|
||||
"Readarr:8787:/readarr"
|
||||
"Bazarr:6767:"
|
||||
"Prowlarr:9696:"
|
||||
"Jellyfin:8096:"
|
||||
"Plex:32400:/web"
|
||||
"Overseerr:5055:"
|
||||
"Jellyseerr:5056:"
|
||||
"Portainer:9000:"
|
||||
"Traefik:8080:"
|
||||
"NPM:81:"
|
||||
"qBittorrent:8082:"
|
||||
"Transmission:9091:"
|
||||
"NZBGet:6789:"
|
||||
"SABnzbd:8080:"
|
||||
"Uptime-Kuma:3001:"
|
||||
"Jellystat:3000:"
|
||||
)
|
||||
|
||||
local active_services=0
|
||||
for service_info in "${services[@]}"; do
|
||||
local service_name="${service_info%%:*}"
|
||||
local service_port="${service_info#*:}"
|
||||
local service_path="${service_port#*:}"
|
||||
service_port="${service_port%:*}"
|
||||
|
||||
if docker ps --format "{{.Names}}" | grep -qi "${service_name,,}"; then
|
||||
local url="http://${local_ip}:${service_port}${service_path}"
|
||||
printf " ${GREEN}●${NC} %-15s %s\n" "$service_name" "$url"
|
||||
((active_services++))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $active_services -eq 0 ]]; then
|
||||
echo -e " ${YELLOW}No services currently running${NC}"
|
||||
fi
|
||||
|
||||
echo -e "\n${YELLOW}💡 Tips:${NC}"
|
||||
echo -e " • Bookmark these URLs for easy access"
|
||||
echo -e " • Default credentials are in the .env file"
|
||||
echo -e " • Change default passwords after first login"
|
||||
echo -e " • Some services may take a few minutes to fully start"
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Show logs
|
||||
show_logs() {
|
||||
show_header
|
||||
echo -e "${WHITE}📋 HOPS Logs${NC}\n"
|
||||
|
||||
if [[ ! -d "$LOG_DIR" ]]; then
|
||||
warning "No log directory found"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Available log files:${NC}"
|
||||
local log_files=($(find "$LOG_DIR" -name "*.log" -type f | sort -r))
|
||||
|
||||
if [[ ${#log_files[@]} -eq 0 ]]; then
|
||||
warning "No log files found"
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
return
|
||||
fi
|
||||
|
||||
local count=1
|
||||
for log_file in "${log_files[@]}"; do
|
||||
local basename_log=$(basename "$log_file")
|
||||
local size=$(du -h "$log_file" | cut -f1)
|
||||
local date=$(stat -c %y "$log_file" | cut -d' ' -f1)
|
||||
|
||||
printf " %d) %-40s (%s, %s)\n" "$count" "$basename_log" "$size" "$date"
|
||||
((count++))
|
||||
done
|
||||
|
||||
echo -e "\n${WHITE}Select a log file to view [1-${#log_files[@]}] or 0 to go back: ${NC}"
|
||||
read -r choice
|
||||
|
||||
if [[ "$choice" -eq 0 ]]; then
|
||||
return
|
||||
elif [[ "$choice" -gt 0 && "$choice" -le ${#log_files[@]} ]]; then
|
||||
local selected_log="${log_files[$((choice-1))]}"
|
||||
echo -e "\n${BLUE}Showing last 50 lines of $(basename "$selected_log"):${NC}\n"
|
||||
tail -50 "$selected_log"
|
||||
else
|
||||
warning "Invalid selection"
|
||||
fi
|
||||
|
||||
echo -e "\n${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Show help information
|
||||
show_help() {
|
||||
show_header
|
||||
echo -e "${WHITE}📚 HOPS Help & Documentation${NC}\n"
|
||||
|
||||
echo -e "${BLUE}🎯 What is HOPS?${NC}"
|
||||
echo -e "HOPS (Homelab Orchestration Provisioning Script) is an automated installer"
|
||||
echo -e "for popular homelab applications including media servers, download clients,"
|
||||
echo -e "and monitoring tools.\n"
|
||||
|
||||
echo -e "${BLUE}🚀 Quick Start:${NC}"
|
||||
echo -e " 1. Run this script as root/sudo"
|
||||
echo -e " 2. Choose 'Install HOPS' from the menu"
|
||||
echo -e " 3. Configure directories and timezone"
|
||||
echo -e " 4. Select your desired services"
|
||||
echo -e " 5. Wait for installation to complete"
|
||||
echo -e " 6. Access services via the provided URLs\n"
|
||||
|
||||
echo -e "${BLUE}📱 Supported Services:${NC}"
|
||||
echo -e " • Media Management: Sonarr, Radarr, Lidarr, Readarr, Bazarr, Prowlarr"
|
||||
echo -e " • Download Clients: qBittorrent, Transmission, NZBGet, SABnzbd"
|
||||
echo -e " • Media Servers: Jellyfin, Plex, Emby, Jellystat"
|
||||
echo -e " • Request Management: Overseerr, Jellyseerr, Ombi"
|
||||
echo -e " • Reverse Proxy: Traefik, Nginx Proxy Manager"
|
||||
echo -e " • Monitoring: Portainer, Uptime Kuma, Watchtower\n"
|
||||
|
||||
echo -e "${BLUE}🔧 Requirements:${NC}"
|
||||
echo -e " • Ubuntu/Debian/Mint Linux"
|
||||
echo -e " • 2GB+ RAM (4GB+ recommended)"
|
||||
echo -e " • 10GB+ free disk space"
|
||||
echo -e " • Root/sudo access"
|
||||
echo -e " • Internet connection\n"
|
||||
|
||||
echo -e "${BLUE}📁 Default Locations:${NC}"
|
||||
echo -e " • Homelab directory: ~/homelab/"
|
||||
echo -e " • App configurations: /opt/appdata/"
|
||||
echo -e " • Media storage: /mnt/media/"
|
||||
echo -e " • Logs: /var/log/hops/\n"
|
||||
|
||||
echo -e "${BLUE}🆘 Troubleshooting:${NC}"
|
||||
echo -e " • Check logs in the 'View Logs' menu"
|
||||
echo -e " • Verify Docker is running: systemctl status docker"
|
||||
echo -e " • Check container status: docker ps"
|
||||
echo -e " • View service logs: docker logs [service-name]"
|
||||
echo -e " • Restart services: docker compose restart [service-name]\n"
|
||||
|
||||
echo -e "${BLUE}🔐 Security Notes:${NC}"
|
||||
echo -e " • Change default passwords in .env file after installation"
|
||||
echo -e " • Configure firewall rules as needed"
|
||||
echo -e " • Regularly update services using the management menu\n"
|
||||
|
||||
echo -e "${WHITE}Press Enter to return to main menu...${NC}"
|
||||
read -r
|
||||
}
|
||||
|
||||
# Main menu
|
||||
show_main_menu() {
|
||||
local status=$(get_installation_status)
|
||||
|
||||
echo -e "${WHITE}🎛️ Main Menu:${NC}"
|
||||
echo -e " 1) Install HOPS"
|
||||
|
||||
if [[ "$status" != "not_installed" ]]; then
|
||||
echo -e " 2) Uninstall HOPS"
|
||||
echo -e " 3) Manage Services"
|
||||
echo -e " 4) Service Status"
|
||||
echo -e " 5) Access Information"
|
||||
else
|
||||
echo -e " 2) Uninstall HOPS ${YELLOW}(not installed)${NC}"
|
||||
echo -e " 3) Manage Services ${YELLOW}(not installed)${NC}"
|
||||
echo -e " 4) Service Status ${YELLOW}(not installed)${NC}"
|
||||
echo -e " 5) Access Information ${YELLOW}(not installed)${NC}"
|
||||
fi
|
||||
|
||||
echo -e " 6) View Logs"
|
||||
echo -e " 7) Help & Documentation"
|
||||
echo -e " 8) Exit"
|
||||
|
||||
echo -e "\n${WHITE}Select an option [1-8]: ${NC}"
|
||||
}
|
||||
|
||||
# Main program loop
|
||||
main() {
|
||||
init_logging
|
||||
check_root
|
||||
check_dependencies
|
||||
|
||||
while true; do
|
||||
show_header
|
||||
show_status
|
||||
show_main_menu
|
||||
|
||||
read -r choice
|
||||
|
||||
case $choice in
|
||||
1)
|
||||
check_system_requirements
|
||||
run_installer
|
||||
;;
|
||||
2)
|
||||
run_uninstaller
|
||||
;;
|
||||
3)
|
||||
manage_services
|
||||
;;
|
||||
4)
|
||||
show_service_status
|
||||
;;
|
||||
5)
|
||||
show_access_info
|
||||
;;
|
||||
6)
|
||||
show_logs
|
||||
;;
|
||||
7)
|
||||
show_help
|
||||
;;
|
||||
8)
|
||||
info "Thank you for using HOPS!"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
warning "Invalid option. Please select 1-8."
|
||||
sleep 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Script entry point
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
Executable
+924
@@ -0,0 +1,924 @@
|
||||
#!/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
|
||||
}
|
||||
Executable
+1285
File diff suppressed because it is too large
Load Diff
Executable
+560
@@ -0,0 +1,560 @@
|
||||
#!/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
|
||||
}
|
||||
Reference in New Issue
Block a user