Add EZ-Homelab Enhanced Setup System

- Complete modular bash-based setup system replacing Python TUI
- Phase 1-4 implementation: Core Infrastructure, Configuration Management, Deployment Engine, Service Orchestration & Management
- 9 production-ready scripts: preflight.sh, setup.sh, pre-deployment-wizard.sh, localize.sh, generalize.sh, validate.sh, deploy.sh, service.sh, monitor.sh, backup.sh, update.sh
- Shared libraries: common.sh (utilities), ui.sh (text interface)
- Template-based configuration system with environment variable substitution
- Comprehensive documentation: PRD, standards, and quick reference guides
- Automated backup, monitoring, and update management capabilities
- Cross-platform compatibility with robust error handling and logging
This commit is contained in:
Kelin
2026-01-29 19:53:36 -05:00
parent dd4ff47048
commit f141848a10
19 changed files with 6605 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
# Authelia Configuration
# Copy to /opt/stacks/authelia/configuration.yml
# IMPORTANT: Replace '${DOMAIN}' with your actual DuckDNS domain
server:
host: 0.0.0.0
port: 9091
log:
level: info
theme: dark
jwt_secret: ${AUTHELIA_JWT_SECRET}
default_redirection_url: https://auth.${DOMAIN}
totp:
issuer: ${DOMAIN}
period: 30
skew: 1
authentication_backend:
file:
path: /config/users_database.yml
password:
algorithm: argon2id
iterations: 1
key_length: 32
salt_length: 16
memory: 1024
parallelism: 8
access_control:
default_policy: deny
rules:
# Bypass Authelia for Jellyfin (allow app access)
- domain: jellyfin.${DOMAIN}
policy: bypass
# Bypass for Plex (allow app access)
- domain: plex.${DOMAIN}
policy: bypass
# Bypass for Home Assistant (has its own auth)
- domain: ha.${DOMAIN}
policy: bypass
# Bypass for development services (they have their own auth or setup)
- domain: pgadmin.${DOMAIN}
policy: bypass
- domain: gitlab.${DOMAIN}
policy: bypass
# Protected: All other services require authentication
- domain: "*.${DOMAIN}"
policy: one_factor
# Two-factor for admin services (optional)
# - domain:
# - "admin.${DOMAIN}"
# - "portainer.${DOMAIN}"
# policy: two_factor
session:
name: authelia_session
secret: ${AUTHELIA_SESSION_SECRET}
expiration: 24h # Session expires after 24 hours
inactivity: 24h # Session expires after 24 hours of inactivity
remember_me_duration: 1M
domain: ${DOMAIN}
regulation:
max_retries: 3
find_time: 2m
ban_time: 5m
storage:
encryption_key: ${AUTHELIA_STORAGE_ENCRYPTION_KEY}
local:
path: /data/db.sqlite3
notifier:
# File-based notifications (for development/testing)
filesystem:
filename: /data/notification.txt

View File

@@ -0,0 +1,12 @@
###############################################################
# Users Database #
###############################################################
users:
kelin:
displayname: "Admin User"
password: "$argon2id$v=19$m=65536,t=3,p=4$a+3pIrywP/li9wy9J6UkMA$+3THyJiAnS/gNYnLaYtlsRCaYfgnnxsUyGZ4D3xGnUg"
email: ${DEFAULT_EMAIL}
groups:
- admins
- users

View File

@@ -0,0 +1,139 @@
# Core Infrastructure Services
# These services form the foundation of the homelab and should always be running
# Place in /opt/stacks/core/docker-compose.yml
# RESTART POLICY GUIDE:
# - unless-stopped: Core infrastructure services that should always run
# - no: Services with Sablier lazy loading (start on-demand)
# - See individual service comments for specific reasoning
services:
duckdns:
# Dynamic DNS service - must always run to maintain domain resolution
image: lscr.io/linuxserver/duckdns:latest
container_name: duckdns
restart: unless-stopped
environment:
- PUID=${PUID}
- PGID=${PGID}
- TZ=${TZ}
- SUBDOMAINS=${DUCKDNS_SUBDOMAINS}
- TOKEN=${DUCKDNS_TOKEN}
volumes:
- ./duckdns/config:/config
networks:
- traefik-network
traefik:
# Reverse proxy and SSL termination - core routing service, must always run
image: traefik:v3
container_name: traefik
restart: unless-stopped
command: ["--configFile=/config/traefik.yml"]
environment:
- DUCKDNS_TOKEN=${DUCKDNS_TOKEN}
ports:
- 80:80
- 443:443
- 8080:8080
volumes:
- ./traefik/config:/config
- ./traefik/letsencrypt:/letsencrypt
- ./traefik/dynamic:/dynamic
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- traefik-network
labels:
# TRAEFIK CONFIGURATION
# ==========================================
# Service metadata
- "homelab.category=core"
- "homelab.description=Reverse proxy and SSL termination"
# Traefik reverse proxy (comment/uncomment to disable/enable)
# If Traefik is on a remote server: these labels are NOT USED;
# configure external yml files in /traefik/dynamic folder instead.
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.routers.traefik.middlewares=authelia@docker"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
authelia:
# Single sign-on authentication service - must always run for user authentication
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
environment:
- TZ=${TZ}
ports:
- "9091:9091"
volumes:
- ./authelia/config:/config
- ./authelia/secrets:/secrets
networks:
- traefik-network
depends_on:
- traefik
labels:
# TRAEFIK CONFIGURATION
# ==========================================
# Service metadata
- "homelab.category=core"
- "homelab.description=Single sign-on authentication"
# Traefik reverse proxy (comment/uncomment to disable/enable)
# If Traefik is on a remote server: these labels are NOT USED;
# configure external yml files in /traefik/dynamic folder instead.
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host(`auth.${DOMAIN}`)"
- "traefik.http.routers.authelia.entrypoints=websecure"
- "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
- "traefik.http.routers.authelia.service=authelia"
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
# Authelia forward auth middleware configuration
- "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.${DOMAIN}/"
- "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=X-Secret"
- "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true"
# Sablier - Lazy loading service for Docker containers
# Controls startup/shutdown of lazy-loaded services, must always run
# REQUIREMENTS FOR DOCKER API ACCESS:
# 1. Docker daemon must be configured to listen on TCP port 2376 with TLS
# 2. DOCKER_HOST environment variable must point to accessible Docker API endpoint
# 3. Firewall must allow TCP connections to Docker API port (2376)
# 4. TLS certificates must be mounted and environment variables set
# 5. Ensure dockerproxy service is running and accessible
sablier-service:
image: sablierapp/sablier:latest
container_name: sablier-service
restart: unless-stopped
networks:
- traefik-network
environment:
- SABLIER_PROVIDER=docker
- SABLIER_DOCKER_API_VERSION=1.51
- SABLIER_DOCKER_NETWORK=traefik-network
- SABLIER_LOG_LEVEL=debug
- DOCKER_HOST=tcp://${SERVER_IP}:2376
- DOCKER_TLS_VERIFY=1
- DOCKER_CERT_PATH=/certs
volumes:
- ./shared-ca:/certs:ro
ports:
- 10000:10000
labels:
# Service metadata
- "homelab.category=core"
- "homelab.description=Lazy loading service for Docker containers"
networks:
traefik-network:
external: true
x-dockge:
urls:
- https://auth.${DOMAIN}
- http://${SERVER_IP}:9091
- https://traefik.${DOMAIN}
- http://${SERVER_IP}:8080

View File

@@ -0,0 +1,111 @@
# Traefik Routing Quick Reference
## Variables (used throughout):
```yaml
${infrastructure}
${description}
${watchtower_enable}
${service}
${sso_enable}
${sablier_enable}
${traefik_enable}
${port}
```
## Compose file labels section
### Service metadata
```yaml
- homelab.category=${infrastructure}
- homelab.description=${description}
- com.centurylinklabs.watchtower.enable=${watchtower_enable}
```
### Traefik labels
>**Traefik labels** are used for services on the same machine
They are ignored when the service is on a different machine
```yaml
- "traefik.enable=${traefik_enable}"
- "traefik.http.routers.${service}.rule=Host(`${service}.${DOMAIN}`)"
- "traefik.http.routers.${service}.entrypoints=websecure"
- "traefik.http.routers.${service}.tls.certresolver=letsencrypt"
- "traefik.http.routers.${service}.middlewares=authelia@docker"
- "traefik.http.services.${service}.loadbalancer.server.port=${port}"
```
### Sablier lazy loading
```yaml
- sablier.enable=${sablier_enable}
- sablier.group=${SERVER_HOSTNAME}-${service}
- sablier.start-on-demand=true
```
## External Host Yml Files
>**Recomended**: use 1 yml file per host
### external-host-production.yml
```yaml
http:
# Routes for External Host Services
routers:
# External Service Routing Template
${service}-${SERVER_HOSTNAME}:
rule: "Host(`${service}.${DOMAIN}`)"
entryPoints:
- websecure
service: ${service}-${SERVER_HOSTNAME}
tls:
certResolver: letsencrypt
middlewares:
- sablier-${SERVER_HOSTNAME}-${service}@file
- authelia@docker
# Middleware Definitions
middlewares:
# Service Definitions
services:
${service}-${SERVER_HOSTNAME}:
loadBalancer:
servers:
- url: "http://${SERVER_IP}:${port}"
passHostHeader: true
```
## sablier.yml
```yaml
# Session duration set to 5m for testing. Increase to 30m for production.
http:
middlewares:
# Authelia SSO middleware
authelia:
forwardauth:
address: http://authelia:9091/api/verify?rd=https://auth.${DOMAIN}/
authResponseHeaders:
- X-Secret
trustForwardHeader: true
# Sablier enabled Service Template
sablier-${SERVER_HOSTNAME}-${service}:
plugin:
sablier:
sablierUrl: http://sablier-service:10000
group: ${SERVER_HOSTNAME}-${service}
sessionDuration: 5m
ignoreUserAgent: curl
dynamic:
displayName: ${service}
theme: ghost
show-details-by-default: true
```

686
scripts/enhanced-setup/backup.sh Executable file
View File

@@ -0,0 +1,686 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Backup Management
# Automated backup orchestration and restore operations
SCRIPT_NAME="backup"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# BACKUP CONFIGURATION
# =============================================================================
# Backup directories
BACKUP_ROOT="${BACKUP_ROOT:-$HOME/.ez-homelab/backups}"
BACKUP_CONFIG="${BACKUP_CONFIG:-$BACKUP_ROOT/config}"
BACKUP_DATA="${BACKUP_DATA:-$BACKUP_ROOT/data}"
BACKUP_LOGS="${BACKUP_LOGS:-$BACKUP_ROOT/logs}"
# Backup retention (days)
CONFIG_RETENTION_DAYS=30
DATA_RETENTION_DAYS=7
LOG_RETENTION_DAYS=7
# Backup schedule (cron format)
CONFIG_BACKUP_SCHEDULE="0 2 * * *" # Daily at 2 AM
DATA_BACKUP_SCHEDULE="0 3 * * 0" # Weekly on Sunday at 3 AM
LOG_BACKUP_SCHEDULE="0 1 * * *" # Daily at 1 AM
# Compression settings
COMPRESSION_LEVEL=6
COMPRESSION_TYPE="gzip"
# =============================================================================
# BACKUP UTILITY FUNCTIONS
# =============================================================================
# Initialize backup directories
init_backup_dirs() {
mkdir -p "$BACKUP_CONFIG" "$BACKUP_DATA" "$BACKUP_LOGS"
# Create .gitkeep files to ensure directories are tracked
touch "$BACKUP_CONFIG/.gitkeep" "$BACKUP_DATA/.gitkeep" "$BACKUP_LOGS/.gitkeep"
}
# Generate backup filename with timestamp
generate_backup_filename() {
local prefix="$1"
local timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
echo "${prefix}_${timestamp}.tar.${COMPRESSION_TYPE}"
}
# Compress directory
compress_directory() {
local source_dir="$1"
local archive_path="$2"
print_info "Compressing $source_dir to $archive_path"
case "$COMPRESSION_TYPE" in
gzip)
tar -czf "$archive_path" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
;;
bzip2)
tar -cjf "$archive_path" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
;;
xz)
tar -cJf "$archive_path" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
;;
*)
print_error "Unsupported compression type: $COMPRESSION_TYPE"
return 1
;;
esac
}
# Extract archive
extract_archive() {
local archive_path="$1"
local dest_dir="$2"
print_info "Extracting $archive_path to $dest_dir"
mkdir -p "$dest_dir"
case "$COMPRESSION_TYPE" in
gzip)
tar -xzf "$archive_path" -C "$dest_dir"
;;
bzip2)
tar -xjf "$archive_path" -C "$dest_dir"
;;
xz)
tar -xJf "$archive_path" -C "$dest_dir"
;;
*)
print_error "Unsupported compression type: $COMPRESSION_TYPE"
return 1
;;
esac
}
# Clean old backups
cleanup_old_backups() {
local backup_dir="$1"
local retention_days="$2"
local prefix="$3"
print_info "Cleaning up backups older than ${retention_days} days in $backup_dir"
# Find and remove old backups
find "$backup_dir" -name "${prefix}_*.tar.${COMPRESSION_TYPE}" -type f -mtime +"$retention_days" -exec rm {} \; -print
}
# Get backup size
get_backup_size() {
local backup_path="$1"
if [[ -f "$backup_path" ]]; then
du -h "$backup_path" | cut -f1
else
echo "N/A"
fi
}
# List backups
list_backups() {
local backup_dir="$1"
local prefix="$2"
echo "Backups in $backup_dir:"
echo "----------------------------------------"
local count=0
while IFS= read -r -d '' file; do
local size
size=$(get_backup_size "$file")
local mtime
mtime=$(stat -c %y "$file" 2>/dev/null | cut -d'.' -f1 || echo "Unknown")
printf " %-40s %-8s %s\n" "$(basename "$file")" "$size" "$mtime"
((count++))
done < <(find "$backup_dir" -name "${prefix}_*.tar.${COMPRESSION_TYPE}" -type f -print0 | sort -z)
if (( count == 0 )); then
echo " No backups found"
fi
echo
}
# =============================================================================
# CONFIGURATION BACKUP FUNCTIONS
# =============================================================================
# Backup configuration files
backup_config() {
print_info "Starting configuration backup"
local temp_dir
temp_dir=$(mktemp -d)
local config_dir="$temp_dir/config"
mkdir -p "$config_dir"
# Backup EZ-Homelab configuration
if [[ -d "$EZ_HOME" ]]; then
print_info "Backing up EZ-Homelab configuration"
cp -r "$EZ_HOME/docker-compose" "$config_dir/" 2>/dev/null || true
cp -r "$EZ_HOME/templates" "$config_dir/" 2>/dev/null || true
cp "$EZ_HOME/.env" "$config_dir/" 2>/dev/null || true
fi
# Backup Docker daemon config
if [[ -f "/etc/docker/daemon.json" ]]; then
print_info "Backing up Docker daemon configuration"
mkdir -p "$config_dir/docker"
cp "/etc/docker/daemon.json" "$config_dir/docker/" 2>/dev/null || true
fi
# Backup system configuration
print_info "Backing up system configuration"
mkdir -p "$config_dir/system"
cp "/etc/hostname" "$config_dir/system/" 2>/dev/null || true
cp "/etc/hosts" "$config_dir/system/" 2>/dev/null || true
cp "/etc/resolv.conf" "$config_dir/system/" 2>/dev/null || true
# Create backup archive
local backup_file
backup_file=$(generate_backup_filename "config")
local backup_path="$BACKUP_CONFIG/$backup_file"
if compress_directory "$config_dir" "$backup_path"; then
print_success "Configuration backup completed: $backup_file"
print_info "Backup size: $(get_backup_size "$backup_path")"
# Cleanup old backups
cleanup_old_backups "$BACKUP_CONFIG" "$CONFIG_RETENTION_DAYS" "config"
# Cleanup temp directory
rm -rf "$temp_dir"
return 0
else
print_error "Configuration backup failed"
rm -rf "$temp_dir"
return 1
fi
}
# Restore configuration
restore_config() {
local backup_file="$1"
if [[ -z "$backup_file" ]]; then
print_error "Backup file name required"
return 1
fi
local backup_path="$BACKUP_CONFIG/$backup_file"
if [[ ! -f "$backup_path" ]]; then
print_error "Backup file not found: $backup_path"
return 1
fi
print_warning "This will overwrite existing configuration files. Continue? (y/N)"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
print_info "Configuration restore cancelled"
return 0
fi
print_info "Restoring configuration from $backup_file"
local temp_dir
temp_dir=$(mktemp -d)
if extract_archive "$backup_path" "$temp_dir"; then
local extracted_dir="$temp_dir/config"
# Restore EZ-Homelab configuration
if [[ -d "$extracted_dir/docker-compose" ]]; then
print_info "Restoring EZ-Homelab configuration"
cp -r "$extracted_dir/docker-compose" "$EZ_HOME/" 2>/dev/null || true
fi
if [[ -d "$extracted_dir/templates" ]]; then
cp -r "$extracted_dir/templates" "$EZ_HOME/" 2>/dev/null || true
fi
if [[ -f "$extracted_dir/.env" ]]; then
cp "$extracted_dir/.env" "$EZ_HOME/" 2>/dev/null || true
fi
# Restore Docker configuration
if [[ -f "$extracted_dir/docker/daemon.json" ]]; then
print_info "Restoring Docker daemon configuration"
sudo cp "$extracted_dir/docker/daemon.json" "/etc/docker/" 2>/dev/null || true
fi
# Restore system configuration
if [[ -f "$extracted_dir/system/hostname" ]]; then
print_info "Restoring system hostname"
sudo cp "$extracted_dir/system/hostname" "/etc/" 2>/dev/null || true
fi
print_success "Configuration restore completed"
rm -rf "$temp_dir"
return 0
else
print_error "Configuration restore failed"
rm -rf "$temp_dir"
return 1
fi
}
# =============================================================================
# DATA BACKUP FUNCTIONS
# =============================================================================
# Backup Docker volumes
backup_docker_volumes() {
print_info "Starting Docker volumes backup"
local temp_dir
temp_dir=$(mktemp -d)
local volumes_dir="$temp_dir/volumes"
mkdir -p "$volumes_dir"
# Get all Docker volumes
local volumes
mapfile -t volumes < <(docker volume ls --format "{{.Name}}" 2>/dev/null | grep -E "^ez-homelab|^homelab" || true)
if [[ ${#volumes[@]} -eq 0 ]]; then
print_warning "No EZ-Homelab volumes found to backup"
rm -rf "$temp_dir"
return 0
fi
print_info "Found ${#volumes[@]} volumes to backup"
for volume in "${volumes[@]}"; do
print_info "Backing up volume: $volume"
# Create a temporary container to backup the volume
local container_name="ez_backup_${volume}_$(date +%s)"
if docker run --rm -d --name "$container_name" -v "$volume:/data" alpine sleep 30 >/dev/null 2>&1; then
# Copy volume data
mkdir -p "$volumes_dir/$volume"
docker cp "$container_name:/data/." "$volumes_dir/$volume/" 2>/dev/null || true
# Clean up container
docker stop "$container_name" >/dev/null 2>&1 || true
else
print_warning "Failed to backup volume: $volume"
fi
done
# Create backup archive
local backup_file
backup_file=$(generate_backup_filename "volumes")
local backup_path="$BACKUP_DATA/$backup_file"
if compress_directory "$volumes_dir" "$backup_path"; then
print_success "Docker volumes backup completed: $backup_file"
print_info "Backup size: $(get_backup_size "$backup_path")"
# Cleanup old backups
cleanup_old_backups "$BACKUP_DATA" "$DATA_RETENTION_DAYS" "volumes"
rm -rf "$temp_dir"
return 0
else
print_error "Docker volumes backup failed"
rm -rf "$temp_dir"
return 1
fi
}
# Restore Docker volumes
restore_docker_volumes() {
local backup_file="$1"
if [[ -z "$backup_file" ]]; then
print_error "Backup file name required"
return 1
fi
local backup_path="$BACKUP_DATA/$backup_file"
if [[ ! -f "$backup_path" ]]; then
print_error "Backup file not found: $backup_path"
return 1
fi
print_warning "This will overwrite existing Docker volumes. Continue? (y/N)"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
print_info "Docker volumes restore cancelled"
return 0
fi
print_info "Restoring Docker volumes from $backup_file"
local temp_dir
temp_dir=$(mktemp -d)
if extract_archive "$backup_path" "$temp_dir"; then
local volumes_dir="$temp_dir/volumes"
# Restore each volume
for volume_dir in "$volumes_dir"/*/; do
if [[ -d "$volume_dir" ]]; then
local volume_name
volume_name=$(basename "$volume_dir")
print_info "Restoring volume: $volume_name"
# Create volume if it doesn't exist
docker volume create "$volume_name" >/dev/null 2>&1 || true
# Create temporary container to restore data
local container_name="ez_restore_${volume_name}_$(date +%s)"
if docker run --rm -d --name "$container_name" -v "$volume_name:/data" alpine sleep 30 >/dev/null 2>&1; then
# Copy data back
docker cp "$volume_dir/." "$container_name:/data/" 2>/dev/null || true
# Clean up container
docker stop "$container_name" >/dev/null 2>&1 || true
print_success "Volume restored: $volume_name"
else
print_error "Failed to restore volume: $volume_name"
fi
fi
done
print_success "Docker volumes restore completed"
rm -rf "$temp_dir"
return 0
else
print_error "Docker volumes restore failed"
rm -rf "$temp_dir"
return 1
fi
}
# =============================================================================
# LOG BACKUP FUNCTIONS
# =============================================================================
# Backup logs
backup_logs() {
print_info "Starting logs backup"
local temp_dir
temp_dir=$(mktemp -d)
local logs_dir="$temp_dir/logs"
mkdir -p "$logs_dir"
# Backup EZ-Homelab logs
if [[ -d "$LOG_DIR" ]]; then
print_info "Backing up EZ-Homelab logs"
cp -r "$LOG_DIR"/* "$logs_dir/" 2>/dev/null || true
fi
# Backup Docker logs
print_info "Backing up Docker container logs"
mkdir -p "$logs_dir/docker"
# Get logs from running containers
local containers
mapfile -t containers < <(docker ps --format "{{.Names}}" 2>/dev/null || true)
for container in "${containers[@]}"; do
docker logs "$container" > "$logs_dir/docker/${container}.log" 2>&1 || true
done
# Backup system logs
print_info "Backing up system logs"
mkdir -p "$logs_dir/system"
cp "/var/log/syslog" "$logs_dir/system/" 2>/dev/null || true
cp "/var/log/auth.log" "$logs_dir/system/" 2>/dev/null || true
cp "/var/log/kern.log" "$logs_dir/system/" 2>/dev/null || true
# Create backup archive
local backup_file
backup_file=$(generate_backup_filename "logs")
local backup_path="$BACKUP_LOGS/$backup_file"
if compress_directory "$logs_dir" "$backup_path"; then
print_success "Logs backup completed: $backup_file"
print_info "Backup size: $(get_backup_size "$backup_path")"
# Cleanup old backups
cleanup_old_backups "$BACKUP_LOGS" "$LOG_RETENTION_DAYS" "logs"
rm -rf "$temp_dir"
return 0
else
print_error "Logs backup failed"
rm -rf "$temp_dir"
return 1
fi
}
# =============================================================================
# SCHEDULED BACKUP FUNCTIONS
# =============================================================================
# Setup cron jobs for automated backups
setup_backup_schedule() {
print_info "Setting up automated backup schedule"
# Create backup script
local backup_script="$BACKUP_ROOT/backup.sh"
cat > "$backup_script" << 'EOF'
#!/bin/bash
# Automated backup script for EZ-Homelab
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../scripts/enhanced-setup" && pwd)"
# Run backups
"$SCRIPT_DIR/backup.sh" config --quiet
"$SCRIPT_DIR/backup.sh" volumes --quiet
"$SCRIPT_DIR/backup.sh" logs --quiet
# Log completion
echo "$(date): Automated backup completed" >> "$HOME/.ez-homelab/logs/backup.log"
EOF
chmod +x "$backup_script"
# Add to crontab
local cron_entry
# Config backup (daily at 2 AM)
cron_entry="$CONFIG_BACKUP_SCHEDULE $backup_script config"
if ! crontab -l 2>/dev/null | grep -q "$backup_script config"; then
(crontab -l 2>/dev/null; echo "$cron_entry") | crontab -
print_info "Added config backup to crontab: $cron_entry"
fi
# Data backup (weekly on Sunday at 3 AM)
cron_entry="$DATA_BACKUP_SCHEDULE $backup_script volumes"
if ! crontab -l 2>/dev/null | grep -q "$backup_script volumes"; then
(crontab -l 2>/dev/null; echo "$cron_entry") | crontab -
print_info "Added volumes backup to crontab: $cron_entry"
fi
# Logs backup (daily at 1 AM)
cron_entry="$LOG_BACKUP_SCHEDULE $backup_script logs"
if ! crontab -l 2>/dev/null | grep -q "$backup_script logs"; then
(crontab -l 2>/dev/null; echo "$cron_entry") | crontab -
print_info "Added logs backup to crontab: $cron_entry"
fi
print_success "Automated backup schedule configured"
}
# Remove backup schedule
remove_backup_schedule() {
print_info "Removing automated backup schedule"
# Remove from crontab
crontab -l 2>/dev/null | grep -v "backup.sh" | crontab -
print_success "Automated backup schedule removed"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local action=""
local backup_file=""
local quiet=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << EOF
EZ-Homelab Backup Management
USAGE:
backup [OPTIONS] <ACTION> [BACKUP_FILE]
ACTIONS:
config Backup/restore configuration files
volumes Backup/restore Docker volumes
logs Backup logs
list List available backups
schedule Setup automated backup schedule
unschedule Remove automated backup schedule
all Run all backup types
OPTIONS:
-q, --quiet Suppress non-error output
--restore Restore from backup (requires BACKUP_FILE)
EXAMPLES:
backup config # Backup configuration
backup config --restore config_20240129_020000.tar.gz
backup volumes # Backup Docker volumes
backup logs # Backup logs
backup list config # List config backups
backup schedule # Setup automated backups
backup all # Run all backup types
EOF
exit 0
;;
-q|--quiet)
quiet=true
shift
;;
--restore)
action="restore"
shift
;;
config|volumes|logs|list|schedule|unschedule|all)
action="$1"
shift
break
;;
*)
if [[ -z "$backup_file" ]]; then
backup_file="$1"
else
print_error "Too many arguments"
exit 1
fi
shift
;;
esac
done
# Handle remaining arguments
while [[ $# -gt 0 ]]; do
if [[ -z "$backup_file" ]]; then
backup_file="$1"
else
print_error "Too many arguments"
exit 1
fi
shift
done
# Initialize script
init_script "$SCRIPT_NAME" "$SCRIPT_VERSION"
init_logging "$SCRIPT_NAME"
init_backup_dirs
# Check prerequisites
if ! command_exists "tar"; then
print_error "tar command not found. Please install tar."
exit 1
fi
# Execute action
case "$action" in
config)
if [[ "$action" == "restore" ]]; then
restore_config "$backup_file"
else
backup_config
fi
;;
volumes)
if [[ "$action" == "restore" ]]; then
restore_docker_volumes "$backup_file"
else
backup_docker_volumes
fi
;;
logs)
backup_logs
;;
list)
case "$backup_file" in
config|"")
list_backups "$BACKUP_CONFIG" "config"
;;
volumes)
list_backups "$BACKUP_DATA" "volumes"
;;
logs)
list_backups "$BACKUP_LOGS" "logs"
;;
*)
print_error "Unknown backup type: $backup_file"
exit 1
;;
esac
;;
schedule)
setup_backup_schedule
;;
unschedule)
remove_backup_schedule
;;
all)
print_info "Running all backup types"
backup_config && backup_docker_volumes && backup_logs
;;
"")
print_error "No action specified. Use --help for usage information."
exit 1
;;
*)
print_error "Unknown action: $action"
exit 1
;;
esac
}
# Run main function
main "$@"

440
scripts/enhanced-setup/deploy.sh Executable file
View File

@@ -0,0 +1,440 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Deployment Engine
# Orchestrated deployment of services with proper sequencing and health checks
SCRIPT_NAME="deploy"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# DEPLOYMENT CONFIGURATION
# =============================================================================
# Service deployment order (dependencies must come first)
DEPLOYMENT_ORDER=(
"core" # Infrastructure services (Traefik, Authelia, etc.)
"infrastructure" # Development tools (code-server, etc.)
"dashboards" # Homepage, monitoring dashboards
"monitoring" # Grafana, Prometheus, Loki
"media" # Plex, Jellyfin, etc.
"media-management" # Sonarr, Radarr, etc.
"home" # Home Assistant, Node-RED
"productivity" # Nextcloud, Gitea, etc.
"utilities" # Duplicati, FreshRSS, etc.
"vpn" # VPN services
"alternatives" # Alternative services
"wikis" # Wiki services
)
# Core services that must be running for the system to function
CORE_SERVICES=("traefik" "authelia" "duckdns")
# Service health check timeouts (seconds)
HEALTH_CHECK_TIMEOUT=300
SERVICE_STARTUP_TIMEOUT=60
# =============================================================================
# DEPLOYMENT FUNCTIONS
# =============================================================================
# Get list of available service stacks
get_available_stacks() {
local stacks=()
local stack_dir="$EZ_HOME/docker-compose"
if [[ -d "$stack_dir" ]]; then
while IFS= read -r -d '' dir; do
local stack_name="$(basename "$dir")"
if [[ -f "$dir/docker-compose.yml" ]]; then
stacks+=("$stack_name")
fi
done < <(find "$stack_dir" -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
fi
printf '%s\n' "${stacks[@]}"
}
# Check if a service stack exists
stack_exists() {
local stack="$1"
local stack_dir="$EZ_HOME/docker-compose/$stack"
[[ -d "$stack_dir" && -f "$stack_dir/docker-compose.yml" ]]
}
# Get services in a stack
get_stack_services() {
local stack="$1"
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then
return 1
fi
# Extract service names from docker-compose.yml
# Look for lines that start at column 0 followed by a service name
sed -n '/^services:/,/^[^ ]/p' "$compose_file" 2>/dev/null | \
grep '^ [a-zA-Z0-9_-]\+:' | \
sed 's/^\s*//' | sed 's/:.*$//' || true
}
# Check if a service is running
is_service_running() {
local service="$1"
docker ps --filter "name=$service" --filter "status=running" --format "{{.Names}}" | grep -q "^${service}$"
}
# Check service health
check_service_health() {
local service="$1"
local timeout="${2:-$HEALTH_CHECK_TIMEOUT}"
local start_time=$(date +%s)
print_info "Checking health of service: $service"
while (( $(date +%s) - start_time < timeout )); do
if is_service_running "$service"; then
# Additional health checks could be added here
# For now, just check if container is running
print_success "Service $service is healthy"
return 0
fi
sleep 5
done
print_error "Service $service failed health check (timeout: ${timeout}s)"
return 1
}
# Deploy a single service stack
deploy_stack() {
local stack="$1"
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then
print_error "Compose file not found: $compose_file"
return 1
fi
print_info "Deploying stack: $stack"
# Validate the compose file first
if ! validate_yaml "$compose_file"; then
print_error "Invalid YAML in $compose_file"
return 1
fi
# Pull images first
print_info "Pulling images for stack: $stack"
if ! docker compose -f "$compose_file" pull; then
print_warning "Failed to pull some images for $stack, continuing..."
fi
# Deploy the stack
print_info "Starting services in stack: $stack"
if ! docker compose -f "$compose_file" up -d; then
print_error "Failed to deploy stack: $stack"
return 1
fi
# Get list of services in this stack
local services
mapfile -t services < <(get_stack_services "$stack")
# Wait for services to start and check health
for service in "${services[@]}"; do
print_info "Waiting for service to start: $service"
sleep "$SERVICE_STARTUP_TIMEOUT"
if ! check_service_health "$service"; then
print_error "Service $service in stack $stack failed health check"
return 1
fi
done
print_success "Successfully deployed stack: $stack"
return 0
}
# Stop a service stack
stop_stack() {
local stack="$1"
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then
print_warning "Compose file not found: $compose_file"
return 0
fi
print_info "Stopping stack: $stack"
if docker compose -f "$compose_file" down; then
print_success "Successfully stopped stack: $stack"
return 0
else
print_error "Failed to stop stack: $stack"
return 1
fi
}
# Rollback deployment
rollback_deployment() {
local failed_stack="$1"
local deployed_stacks=("${@:2}")
print_warning "Rolling back deployment due to failure in: $failed_stack"
# Stop the failed stack first
stop_stack "$failed_stack" || true
# Stop all previously deployed stacks in reverse order
for ((i=${#deployed_stacks[@]}-1; i>=0; i--)); do
local stack="${deployed_stacks[i]}"
if [[ "$stack" != "$failed_stack" ]]; then
stop_stack "$stack" || true
fi
done
print_info "Rollback completed"
}
# Deploy all stacks in order
deploy_all() {
local deployed_stacks=()
local total_stacks=${#DEPLOYMENT_ORDER[@]}
local current_stack=0
print_info "Starting full deployment of $total_stacks stacks"
for stack in "${DEPLOYMENT_ORDER[@]}"; do
current_stack=$((current_stack + 1))
local percent=$(( current_stack * 100 / total_stacks ))
if ui_available && ! $non_interactive; then
ui_gauge "Deploying $stack... ($current_stack/$total_stacks)" "$percent"
fi
print_info "[$current_stack/$total_stacks] Deploying stack: $stack"
if ! stack_exists "$stack"; then
print_warning "Stack $stack not found, skipping"
continue
fi
if deploy_stack "$stack"; then
deployed_stacks+=("$stack")
else
print_error "Failed to deploy stack: $stack"
rollback_deployment "$stack" "${deployed_stacks[@]}"
return 1
fi
done
print_success "All stacks deployed successfully!"
return 0
}
# Deploy specific stacks
deploy_specific() {
local stacks=("$@")
local deployed_stacks=()
local total_stacks=${#stacks[@]}
local current_stack=0
print_info "Starting deployment of $total_stacks specific stacks"
for stack in "${stacks[@]}"; do
current_stack=$((current_stack + 1))
local percent=$(( current_stack * 100 / total_stacks ))
if ui_available && ! $non_interactive; then
ui_gauge "Deploying $stack... ($current_stack/$total_stacks)" "$percent"
fi
print_info "[$current_stack/$total_stacks] Deploying stack: $stack"
if ! stack_exists "$stack"; then
print_error "Stack $stack not found"
rollback_deployment "$stack" "${deployed_stacks[@]}"
return 1
fi
if deploy_stack "$stack"; then
deployed_stacks+=("$stack")
else
print_error "Failed to deploy stack: $stack"
rollback_deployment "$stack" "${deployed_stacks[@]}"
return 1
fi
done
print_success "Specified stacks deployed successfully!"
return 0
}
# Stop all stacks
stop_all() {
local stacks
mapfile -t stacks < <(get_available_stacks)
local total_stacks=${#stacks[@]}
local current_stack=0
print_info "Stopping all $total_stacks stacks"
for stack in "${stacks[@]}"; do
current_stack=$((current_stack + 1))
local percent=$(( current_stack * 100 / total_stacks ))
if ui_available && ! $non_interactive; then
ui_gauge "Stopping $stack... ($current_stack/$total_stacks)" "$percent"
fi
stop_stack "$stack" || true
done
print_success "All stacks stopped"
}
# Show deployment status
show_status() {
print_info "EZ-Homelab Deployment Status"
echo
local stacks
mapfile -t stacks < <(get_available_stacks)
for stack in "${stacks[@]}"; do
echo "Stack: $stack"
local services
mapfile -t services < <(get_stack_services "$stack")
for service in "${services[@]}"; do
if is_service_running "$service"; then
echo "$service - Running"
else
echo "$service - Stopped"
fi
done
echo
done
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local action="deploy"
local stacks=()
local non_interactive=false
local verbose=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << EOF
EZ-Homelab Deployment Engine
USAGE:
deploy [OPTIONS] [ACTION] [STACKS...]
ACTIONS:
deploy Deploy all stacks (default)
stop Stop all stacks
status Show deployment status
restart Restart all stacks
ARGUMENTS:
STACKS Specific stacks to deploy (optional, deploys all if not specified)
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose logging
--no-ui Run without interactive UI
--no-rollback Skip rollback on deployment failure
EXAMPLES:
deploy # Deploy all stacks
deploy core media # Deploy only core and media stacks
deploy stop # Stop all stacks
deploy status # Show status of all services
EOF
exit 0
;;
-v|--verbose)
verbose=true
shift
;;
--no-ui)
non_interactive=true
shift
;;
--no-rollback)
NO_ROLLBACK=true
shift
;;
deploy|stop|status|restart)
action="$1"
shift
break
;;
*)
stacks+=("$1")
shift
;;
esac
done
# Handle remaining arguments as stacks
while [[ $# -gt 0 ]]; do
stacks+=("$1")
shift
done
# Initialize script
init_script "$SCRIPT_NAME" "$SCRIPT_VERSION"
init_logging "$SCRIPT_NAME"
# Check prerequisites
if ! docker_available; then
print_error "Docker is not available. Please run setup.sh first."
exit 1
fi
# Execute action
case "$action" in
deploy)
if [[ ${#stacks[@]} -eq 0 ]]; then
deploy_all
else
deploy_specific "${stacks[@]}"
fi
;;
stop)
stop_all
;;
status)
show_status
;;
restart)
print_info "Restarting all stacks..."
stop_all
sleep 5
deploy_all
;;
*)
print_error "Unknown action: $action"
exit 1
;;
esac
}
# Run main function
main "$@"

View File

@@ -0,0 +1,399 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Configuration Generalization
# Reverse localization by restoring template variables from backups
SCRIPT_NAME="generalize"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# SCRIPT CONFIGURATION
# =============================================================================
# Template variables that were replaced
TEMPLATE_VARS=("DOMAIN" "TZ" "PUID" "PGID" "DUCKDNS_TOKEN" "JWT_SECRET" "SESSION_SECRET" "ENCRYPTION_KEY" "AUTHELIA_ADMIN_PASSWORD" "AUTHELIA_ADMIN_EMAIL" "PLEX_CLAIM_TOKEN" "DEPLOYMENT_TYPE" "SERVER_HOSTNAME" "DOCKER_SOCKET_PATH")
# =============================================================================
# GENERALIZATION FUNCTIONS
# =============================================================================
# Load environment variables
load_environment() {
local env_file="$EZ_HOME/.env"
if [[ ! -f "$env_file" ]]; then
print_error ".env file not found at $env_file"
print_error "Cannot generalize without environment context"
return 1
fi
# Source the .env file
set -a
source "$env_file"
set +a
print_success "Environment loaded from $env_file"
return 0
}
# Find backup template files
find_backup_files() {
local service="${1:-}"
if [[ -n "$service" ]]; then
# Process specific service
local service_dir="$EZ_HOME/docker-compose/$service"
if [[ -d "$service_dir" ]]; then
find "$service_dir" -name "*.template" -type f 2>/dev/null
else
print_error "Service directory not found: $service_dir"
return 1
fi
else
# Process all services
find "$EZ_HOME/docker-compose" -name "*.template" -type f 2>/dev/null
fi
}
# Restore template file from backup
restore_template_file() {
local backup_file="$1"
local original_file="${backup_file%.template}"
print_info "Restoring: $original_file"
if [[ ! -f "$backup_file" ]]; then
print_error "Backup file not found: $backup_file"
return 1
fi
# Confirm destructive operation
if ui_available; then
if ! ui_yesno "Restore $original_file from backup? This will overwrite current changes."; then
print_info "Skipped $original_file"
return 0
fi
fi
# Backup current version (safety)
backup_file "$original_file"
# Restore from template backup
cp "$backup_file" "$original_file"
print_success "Restored $original_file from $backup_file"
return 0
}
# Generalize processed file (reverse engineer values)
generalize_processed_file() {
local file="$1"
local backup_file="${file}.template"
print_info "Generalizing: $file"
if [[ ! -f "$file" ]]; then
print_error "File not found: $file"
return 1
fi
# Create backup if it doesn't exist
if [[ ! -f "$backup_file" ]]; then
cp "$file" "$backup_file"
print_info "Created backup: $backup_file"
fi
# Process template variables in reverse
local temp_file
temp_file=$(mktemp)
cp "$file" "$temp_file"
for var in "${TEMPLATE_VARS[@]}"; do
local value="${!var:-}"
if [[ -n "$value" ]]; then
# Escape special characters in value for sed
local escaped_value
escaped_value=$(printf '%s\n' "$value" | sed 's/[[\.*^$()+?{|]/\\&/g')
# Replace actual values back to ${VAR} format
sed -i "s|$escaped_value|\${$var}|g" "$temp_file"
log_debug "Generalized \${$var} in $file"
fi
done
# Move generalized file back
mv "$temp_file" "$file"
print_success "Generalized $file"
return 0
}
# Clean up backup files
cleanup_backups() {
local service="${1:-}"
print_info "Cleaning up backup files..."
local backup_files
mapfile -t backup_files < <(find_backup_files "$service")
if [[ ${#backup_files[@]} -eq 0 ]]; then
print_info "No backup files to clean up"
return 0
fi
local cleaned=0
for backup in "${backup_files[@]}"; do
if ui_available; then
if ui_yesno "Delete backup file: $backup?"; then
rm -f "$backup"
((cleaned++))
print_info "Deleted: $backup"
fi
else
rm -f "$backup"
((cleaned++))
log_info "Deleted backup: $backup"
fi
done
print_success "Cleaned up $cleaned backup file(s)"
}
# =============================================================================
# UI FUNCTIONS
# =============================================================================
# Show generalization options
show_generalization_menu() {
local text="Select generalization method:"
local items=(
"restore" "Restore from .template backups" "on"
"reverse" "Reverse engineer from current files" "off"
"cleanup" "Clean up backup files" "off"
)
ui_radiolist "$text" "$UI_HEIGHT" "$UI_WIDTH" "${items[@]}"
}
# Show progress for batch processing
show_generalization_progress() {
local total="$1"
local current="$2"
local file="$3"
local percent=$(( current * 100 / total ))
ui_gauge "Generalizing configurations... ($current/$total)" "$percent"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local service=""
local method=""
local non_interactive=false
local verbose=false
local cleanup=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << EOF
EZ-Homelab Configuration Generalization
USAGE:
$SCRIPT_NAME [OPTIONS] [SERVICE]
ARGUMENTS:
SERVICE Specific service to generalize (optional, processes all if not specified)
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose logging
--method METHOD Generalization method: restore, reverse, cleanup
--cleanup Clean up backup files after generalization
--no-ui Run without interactive UI
METHODS:
restore Restore files from .template backups (safe)
reverse Reverse engineer template variables from current values (advanced)
cleanup Remove .template backup files
EXAMPLES:
$SCRIPT_NAME --method restore # Restore all from backups
$SCRIPT_NAME --method reverse traefik # Reverse engineer Traefik
$SCRIPT_NAME --cleanup # Clean up all backups
WARNING:
Generalization can be destructive. Always backup important data first.
EOF
exit 0
;;
-v|--verbose)
verbose=true
;;
--method)
shift
method="$1"
;;
--cleanup)
cleanup=true
;;
--no-ui)
non_interactive=true
;;
-*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
*)
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Multiple services specified. Use only one service name."
exit 1
fi
;;
esac
shift
done
# Initialize script
init_script "$SCRIPT_NAME"
if $verbose; then
set -x
fi
print_info "Starting EZ-Homelab configuration generalization..."
# Load environment
if ! load_environment; then
exit 1
fi
# Determine method
if [[ -z "$method" ]]; then
if ui_available && ! $non_interactive; then
method=$(show_generalization_menu) || exit 1
else
print_error "Method must be specified with --method when running non-interactively"
echo "Available methods: restore, reverse, cleanup"
exit 1
fi
fi
case "$method" in
"restore")
# Find backup files
local backup_files
mapfile -t backup_files < <(find_backup_files "$service")
if [[ ${#backup_files[@]} -eq 0 ]]; then
print_warning "No backup files found"
exit 0
fi
print_info "Found ${#backup_files[@]} backup file(s)"
# Restore files
local restored=0
local total=${#backup_files[@]}
for backup in "${backup_files[@]}"; do
if ui_available && ! $non_interactive; then
show_generalization_progress "$total" "$restored" "$backup"
fi
if restore_template_file "$backup"; then
((restored++))
fi
done
# Close progress gauge
if ui_available && ! $non_interactive; then
ui_gauge "Restoration complete!" 100
sleep 1
fi
print_success "Restored $restored file(s) from backups"
;;
"reverse")
print_warning "Reverse engineering is experimental and may not be perfect"
print_warning "Make sure you have backups of important data"
if ui_available && ! $non_interactive; then
if ! ui_yesno "Continue with reverse engineering? This may modify your configuration files."; then
print_info "Operation cancelled"
exit 0
fi
fi
# Find processed files (those with actual values instead of templates)
local processed_files
mapfile -t processed_files < <(find "$EZ_HOME/docker-compose${service:+/$service}" -name "*.yml" -o -name "*.yaml" -o -name "*.json" -o -name "*.conf" -o -name "*.cfg" -o -name "*.env" 2>/dev/null)
if [[ ${#processed_files[@]} -eq 0 ]]; then
print_warning "No configuration files found"
exit 0
fi
print_info "Found ${#processed_files[@]} file(s) to generalize"
# Generalize files
local generalized=0
local total=${#processed_files[@]}
for file in "${processed_files[@]}"; do
if ui_available && ! $non_interactive; then
show_generalization_progress "$total" "$generalized" "$file"
fi
if generalize_processed_file "$file"; then
((generalized++))
fi
done
# Close progress gauge
if ui_available && ! $non_interactive; then
ui_gauge "Generalization complete!" 100
sleep 1
fi
print_success "Generalized $generalized file(s)"
;;
"cleanup")
cleanup_backups "$service"
;;
*)
print_error "Unknown method: $method"
echo "Available methods: restore, reverse, cleanup"
exit 1
;;
esac
# Optional cleanup
if $cleanup && [[ "$method" != "cleanup" ]]; then
cleanup_backups "$service"
fi
echo ""
print_success "Configuration generalization complete!"
print_info "Use ./validate.sh to check the results"
exit 0
}
# Run main function
main "$@"

View File

@@ -0,0 +1,373 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Common Library
# Shared variables, utility functions, and constants
set -euo pipefail
# =============================================================================
# SHARED VARIABLES
# =============================================================================
# Repository and paths
EZ_HOME="${EZ_HOME:-/home/kelin/EZ-Homelab}"
STACKS_DIR="${STACKS_DIR:-/opt/stacks}"
LOG_DIR="${LOG_DIR:-$HOME/.ez-homelab/logs}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# User and system
EZ_USER="${EZ_USER:-$USER}"
EZ_UID="${EZ_UID:-$(id -u)}"
EZ_GID="${EZ_GID:-$(id -g)}"
# Architecture detection
ARCH="$(uname -m)"
IS_ARM64=false
[[ "$ARCH" == "aarch64" ]] && IS_ARM64=true
# System information
OS_NAME="$(lsb_release -si 2>/dev/null || echo "Unknown")"
OS_VERSION="$(lsb_release -sr 2>/dev/null || echo "Unknown")"
KERNEL_VERSION="$(uname -r)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# =============================================================================
# LOGGING FUNCTIONS
# =============================================================================
# Initialize logging
init_logging() {
local script_name="${1:-unknown}"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/${script_name}.log"
touch "$LOG_FILE"
}
# Log message with timestamp and level
log() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "$timestamp [$SCRIPT_NAME] $level: $message" >> "$LOG_FILE"
echo "$timestamp [$SCRIPT_NAME] $level: $message" >&2
}
# Convenience logging functions
log_info() { log "INFO" "$1"; }
log_warn() { log "WARN" "$1"; }
log_error() { log "ERROR" "$1"; }
log_debug() { log "DEBUG" "$1"; }
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check if running as root
is_root() {
[[ $EUID -eq 0 ]]
}
# Get available disk space in GB
get_disk_space() {
local path="${1:-/}"
df -BG "$path" 2>/dev/null | tail -1 | awk '{print $4}' | sed 's/G//' || echo "0"
}
# Get total memory in MB
get_total_memory() {
free -m 2>/dev/null | awk 'NR==2{printf "%.0f", $2}' || echo "0"
}
# Get available memory in MB
get_available_memory() {
free -m 2>/dev/null | awk 'NR==2{printf "%.0f", $7}' || echo "0"
}
# Check if service is running (systemd)
service_running() {
local service="$1"
systemctl is-active --quiet "$service" 2>/dev/null
}
# Check if Docker is installed and running
docker_available() {
command_exists docker && service_running docker
}
# Check network connectivity
check_network() {
ping -c 1 -W 5 8.8.8.8 >/dev/null 2>&1
}
# Validate YAML file syntax
validate_yaml() {
local file="$1"
if command_exists python3 && python3 -c "import yaml" 2>/dev/null; then
python3 -c "import yaml; yaml.safe_load(open('$file'))" 2>/dev/null
elif command_exists yq; then
yq eval '.' "$file" >/dev/null 2>/dev/null
elif command_exists docker && docker compose version >/dev/null 2>&1; then
# Fallback to docker compose config
local dir=$(dirname "$file")
local base=$(basename "$file")
(cd "$dir" && docker compose -f "$base" config >/dev/null 2>&1)
else
# No validation tools available, assume valid
return 0
fi
}
# Backup file with timestamp
backup_file() {
local file="$1"
local backup="${file}.bak.$(date +%Y%m%d_%H%M%S)"
cp "$file" "$backup"
log_info "Backed up $file to $backup"
}
# Clean up old backups (keep last 5)
cleanup_backups() {
local file="$1"
local backups
mapfile -t backups < <(ls -t "${file}.bak."* 2>/dev/null | tail -n +6)
for backup in "${backups[@]}"; do
rm -f "$backup"
log_debug "Cleaned up old backup: $backup"
done
}
# Display colored message
print_color() {
local color="$1"
local message="$2"
echo -e "${color}${message}${NC}"
}
# Display success message
print_success() {
print_color "$GREEN" "$1"
}
# Display warning message
print_warning() {
print_color "$YELLOW" "$1"
}
# Display error message
print_error() {
print_color "$RED" "$1"
}
# Display info message
print_info() {
print_color "$BLUE" " $1"
}
# =============================================================================
# VALIDATION FUNCTIONS
# =============================================================================
# Validate OS compatibility
validate_os() {
case "$OS_NAME" in
"Ubuntu"|"Debian"|"Raspbian")
if [[ "$OS_NAME" == "Ubuntu" && "$OS_VERSION" =~ ^(20|22|24) ]]; then
return 0
elif [[ "$OS_NAME" == "Debian" && "$OS_VERSION" =~ ^(11|12) ]]; then
return 0
elif [[ "$OS_NAME" == "Raspbian" ]]; then
return 0
fi
;;
esac
return 1
}
# Validate architecture
validate_arch() {
[[ "$ARCH" == "x86_64" || "$ARCH" == "aarch64" ]]
}
# Validate minimum requirements
validate_requirements() {
local min_disk=20 # GB
local min_memory=1024 # MB
local disk_space
disk_space=$(get_disk_space)
local total_memory
total_memory=$(get_total_memory)
if (( disk_space < min_disk )); then
log_error "Insufficient disk space: ${disk_space}GB available, ${min_disk}GB required"
return 1
fi
if (( total_memory < min_memory )); then
log_error "Insufficient memory: ${total_memory}MB available, ${min_memory}MB required"
return 1
fi
return 0
}
# =============================================================================
# DEPENDENCY CHECKS
# =============================================================================
# Check if required packages are installed
check_dependencies() {
local deps=("curl" "wget" "jq" "git")
local missing=()
for dep in "${deps[@]}"; do
if ! command_exists "$dep"; then
missing+=("$dep")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
log_warn "Missing dependencies: ${missing[*]}"
return 1
fi
return 0
}
# Install missing dependencies
install_dependencies() {
if ! check_dependencies; then
log_info "Installing missing dependencies..."
if is_root; then
apt update && apt install -y curl wget jq git
else
sudo apt update && sudo apt install -y curl wget jq git
fi
fi
}
# =============================================================================
# SCRIPT INITIALIZATION
# =============================================================================
# Initialize script environment
init_script() {
local script_name="$1"
SCRIPT_NAME="$script_name"
init_logging "$script_name"
log_info "Starting $script_name on $OS_NAME $OS_VERSION ($ARCH)"
# Set trap for cleanup
trap 'log_error "Script interrupted"; exit 1' INT TERM
# Validate basic requirements
if ! validate_os; then
print_error "Unsupported OS: $OS_NAME $OS_VERSION"
log_error "Unsupported OS: $OS_NAME $OS_VERSION"
exit 1
fi
if ! validate_arch; then
print_error "Unsupported architecture: $ARCH"
log_error "Unsupported architecture: $ARCH"
exit 1
fi
}
# =============================================================================
# DOCKER UTILITIES
# =============================================================================
# Check if Docker is available
docker_available() {
command_exists "docker" && docker info >/dev/null 2>&1
}
# Get services in a stack
get_stack_services() {
local stack="$1"
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then
return 1
fi
# Extract service names from docker-compose.yml
# Look for lines that start at column 0 followed by a service name
sed -n '/^services:/,/^[^ ]/p' "$compose_file" 2>/dev/null | \
grep '^ [a-zA-Z0-9_-]\+:' | \
sed 's/^\s*//' | sed 's/:.*$//' || true
}
# Check if a service is running
is_service_running() {
local service="$1"
docker ps --filter "name=$service" --filter "status=running" --format "{{.Names}}" | grep -q "^${service}$"
}
# Find all available services across all stacks
find_all_services() {
local services=()
# Get all docker-compose directories
local compose_dirs
mapfile -t compose_dirs < <(find "$EZ_HOME/docker-compose" -name "docker-compose.yml" -type f -exec dirname {} \; 2>/dev/null)
for dir in "${compose_dirs[@]}"; do
local stack_services
mapfile -t stack_services < <(get_stack_services "$(basename "$dir")")
for service in "${stack_services[@]}"; do
# Avoid duplicates
if [[ ! " ${services[*]} " =~ " ${service} " ]]; then
services+=("$service")
fi
done
done
printf '%s\n' "${services[@]}" | sort
}
# Find which stack a service belongs to
find_service_stack() {
local service="$1"
local compose_dirs
mapfile -t compose_dirs < <(find "$EZ_HOME/docker-compose" -name "docker-compose.yml" -type f -exec dirname {} \; 2>/dev/null)
for dir in "${compose_dirs[@]}"; do
local stack_services
mapfile -t stack_services < <(get_stack_services "$(basename "$dir")")
for stack_service in "${stack_services[@]}"; do
if [[ "$stack_service" == "$service" ]]; then
echo "$dir"
return 0
fi
done
done
return 1
}
# Get service compose file
get_service_compose_file() {
local service="$1"
local stack_dir
stack_dir=$(find_service_stack "$service")
[[ -n "$stack_dir" ]] && echo "$stack_dir/docker-compose.yml"
}

324
scripts/enhanced-setup/lib/ui.sh Executable file
View File

@@ -0,0 +1,324 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - UI Library
# Dialog/whiptail helper functions for consistent user interface
# Detect available UI tool
if command_exists whiptail; then
UI_TOOL="whiptail"
elif command_exists dialog; then
UI_TOOL="dialog"
else
echo "Error: Neither whiptail nor dialog is installed. Please install one of them."
exit 1
fi
# UI configuration
UI_HEIGHT=20
UI_WIDTH=70
UI_TITLE="EZ-Homelab Setup"
UI_BACKTITLE="EZ-Homelab Enhanced Setup Scripts v1.0"
# Colors (for dialog)
if [[ "$UI_TOOL" == "dialog" ]]; then
export DIALOGRC="$SCRIPT_DIR/lib/dialogrc"
fi
# =============================================================================
# BASIC UI FUNCTIONS
# =============================================================================
# Display a message box
ui_msgbox() {
local text="$1"
local height="${2:-$UI_HEIGHT}"
local width="${3:-$UI_WIDTH}"
"$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--msgbox "$text" "$height" "$width"
}
# Display a yes/no question
ui_yesno() {
local text="$1"
local height="${2:-$UI_HEIGHT}"
local width="${3:-$UI_WIDTH}"
"$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--yesno "$text" "$height" "$width"
}
# Get user input
ui_inputbox() {
local text="$1"
local default="${2:-}"
local height="${3:-$UI_HEIGHT}"
local width="${4:-$UI_WIDTH}"
"$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--inputbox "$text" "$height" "$width" "$default" 2>&1
}
# Display a menu
ui_menu() {
local text="$1"
local height="${2:-$UI_HEIGHT}"
local width="${3:-$UI_WIDTH}"
shift 2
local menu_items=("$@")
local menu_height=$(( ${#menu_items[@]} / 2 ))
"$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--menu "$text" "$height" "$width" "$menu_height" \
"${menu_items[@]}" 2>&1
}
# Display a checklist
ui_checklist() {
local text="$1"
local height="${2:-$UI_HEIGHT}"
local width="${3:-$UI_WIDTH}"
shift 2
local checklist_items=("$@")
local list_height=$(( ${#checklist_items[@]} / 3 ))
"$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--checklist "$text" "$height" "$width" "$list_height" \
"${checklist_items[@]}" 2>&1
}
# Display a radiolist
ui_radiolist() {
local text="$1"
local height="${2:-$UI_HEIGHT}"
local width="${3:-$UI_WIDTH}"
shift 2
local radiolist_items=("$@")
local list_height=$(( ${#radiolist_items[@]} / 3 ))
"$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--radiolist "$text" "$height" "$width" "$list_height" \
"${radiolist_items[@]}" 2>&1
}
# Display progress gauge
ui_gauge() {
local text="$1"
local percent="${2:-0}"
local height="${3:-$UI_HEIGHT}"
local width="${4:-$UI_WIDTH}"
{
echo "$percent"
echo "$text"
} | "$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--gauge "$text" "$height" "$width" 0
}
# =============================================================================
# ADVANCED UI FUNCTIONS
# =============================================================================
# Display progress with updating percentage
ui_progress() {
local title="$1"
local command="$2"
local height="${3:-$UI_HEIGHT}"
local width="${4:-$UI_WIDTH}"
{
eval "$command" | while IFS= read -r line; do
# Try to extract percentage from output
if [[ "$line" =~ ([0-9]+)% ]]; then
echo "${BASH_REMATCH[1]}"
fi
echo "$line" >&2
done
echo "100"
} 2>&1 | "$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--gauge "$title" "$height" "$width" 0
}
# Display a form with multiple fields
ui_form() {
local text="$1"
local height="${2:-$UI_HEIGHT}"
local width="${3:-$UI_WIDTH}"
shift 2
local form_items=("$@")
local form_height=$(( ${#form_items[@]} / 2 ))
"$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--form "$text" "$height" "$width" "$form_height" \
"${form_items[@]}" 2>&1
}
# Display password input (hidden)
ui_password() {
local text="$1"
local height="${2:-$UI_HEIGHT}"
local width="${3:-$UI_WIDTH}"
"$UI_TOOL" --backtitle "$UI_BACKTITLE" --title "$UI_TITLE" \
--passwordbox "$text" "$height" "$width" 2>&1
}
# =============================================================================
# EZ-HOMELAB SPECIFIC UI FUNCTIONS
# =============================================================================
# Display deployment type selection
ui_select_deployment_type() {
local text="Select your deployment type:"
local items=(
"core" "Core Only" "off"
"single" "Single Server (Core + Infrastructure + Services)" "on"
"remote" "Remote Server (Infrastructure + Services only)" "off"
)
ui_radiolist "$text" "$UI_HEIGHT" "$UI_WIDTH" "${items[@]}"
}
# Display service selection checklist
ui_select_services() {
local deployment_type="$1"
local text="Select services to deploy:"
local items=()
case "$deployment_type" in
"core")
items=(
"duckdns" "DuckDNS (Dynamic DNS)" "on"
"traefik" "Traefik (Reverse Proxy)" "on"
"authelia" "Authelia (SSO Authentication)" "on"
"gluetun" "Gluetun (VPN Client)" "on"
"sablier" "Sablier (Lazy Loading)" "on"
)
;;
"single")
items=(
"core" "Core Services" "on"
"infrastructure" "Infrastructure (Dockge, Pi-hole)" "on"
"dashboards" "Dashboards (Homepage, Homarr)" "on"
"media" "Media Services (Plex, Jellyfin)" "off"
"media-management" "Media Management (*arr services)" "off"
"homeassistant" "Home Assistant Stack" "off"
"productivity" "Productivity (Nextcloud, Gitea)" "off"
"monitoring" "Monitoring (Grafana, Prometheus)" "off"
"utilities" "Utilities (Duplicati, FreshRSS)" "off"
)
;;
"remote")
items=(
"infrastructure" "Infrastructure (Dockge, Pi-hole)" "on"
"dashboards" "Dashboards (Homepage, Homarr)" "on"
"media" "Media Services (Plex, Jellyfin)" "off"
"media-management" "Media Management (*arr services)" "off"
"homeassistant" "Home Assistant Stack" "off"
"productivity" "Productivity (Nextcloud, Gitea)" "off"
"monitoring" "Monitoring (Grafana, Prometheus)" "off"
"utilities" "Utilities (Duplicati, FreshRSS)" "off"
)
;;
esac
ui_checklist "$text" "$UI_HEIGHT" "$UI_WIDTH" "${items[@]}"
}
# Display environment configuration form
ui_configure_environment() {
local text="Configure your environment:"
local items=(
"Domain" 1 1 "" 1 20 50 0
"Timezone" 2 1 "America/New_York" 2 20 50 0
"PUID" 3 1 "1000" 3 20 50 0
"PGID" 4 1 "1000" 4 20 50 0
)
ui_form "$text" "$UI_HEIGHT" "$UI_WIDTH" "${items[@]}"
}
# Display confirmation dialog
ui_confirm_action() {
local action="$1"
local details="${2:-}"
local text="Confirm $action?"
if [[ -n "$details" ]]; then
text="$text\n\n$details"
fi
ui_yesno "$text"
}
# Display error and offer retry
ui_error_retry() {
local error="$1"
local suggestion="${2:-}"
local text="Error: $error"
if [[ -n "$suggestion" ]]; then
text="$text\n\nSuggestion: $suggestion"
fi
text="$text\n\nWould you like to retry?"
ui_yesno "$text"
}
# Display success message
ui_success() {
local message="$1"
ui_msgbox "Success!\n\n$message"
}
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
# Check if UI is available (for non-interactive mode)
ui_available() {
[[ -n "${DISPLAY:-}" ]] || [[ -n "${TERM:-}" ]] && [[ "$TERM" != "dumb" ]]
}
# Run command with UI progress if available
run_with_progress() {
local title="$1"
local command="$2"
if ui_available; then
ui_progress "$title" "$command"
else
print_info "$title"
eval "$command"
fi
}
# Display help text
ui_show_help() {
local script_name="$1"
local help_text="
EZ-Homelab $script_name
USAGE:
$script_name [OPTIONS]
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose logging
-y, --yes Assume yes for all prompts
--no-ui Run without interactive UI
EXAMPLES:
$script_name # Interactive mode
$script_name --no-ui # Non-interactive mode
$script_name --help # Show help
For more information, visit:
https://github.com/your-repo/EZ-Homelab
"
echo "$help_text" | ui_msgbox "Help - $script_name" 20 70
}

View File

@@ -0,0 +1,296 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Configuration Localization
# Replace template variables in service configurations with environment values
SCRIPT_NAME="localize"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# SCRIPT CONFIGURATION
# =============================================================================
# Template variables to replace
TEMPLATE_VARS=("DOMAIN" "TZ" "PUID" "PGID" "DUCKDNS_TOKEN" "DUCKDNS_SUBDOMAINS" "AUTHELIA_JWT_SECRET" "AUTHELIA_SESSION_SECRET" "AUTHELIA_STORAGE_ENCRYPTION_KEY" "DEFAULT_EMAIL" "SERVER_IP" "JWT_SECRET" "SESSION_SECRET" "ENCRYPTION_KEY" "AUTHELIA_ADMIN_PASSWORD" "AUTHELIA_ADMIN_EMAIL" "PLEX_CLAIM_TOKEN" "DEPLOYMENT_TYPE" "SERVER_HOSTNAME" "DOCKER_SOCKET_PATH")
# File extensions to process
TEMPLATE_EXTENSIONS=("yml" "yaml" "json" "conf" "cfg" "env")
# =============================================================================
# LOCALIZATION FUNCTIONS
# =============================================================================
# Load environment variables
load_environment() {
local env_file="$EZ_HOME/.env"
if [[ ! -f "$env_file" ]]; then
print_error ".env file not found at $env_file"
print_error "Run ./pre-deployment-wizard.sh first"
return 1
fi
# Source the .env file
set -a
source "$env_file"
set +a
print_success "Environment loaded from $env_file"
return 0
}
# Find template files
find_template_files() {
local service="${1:-}"
local files=()
if [[ -n "$service" ]]; then
# Process specific service
local service_dir="$EZ_HOME/docker-compose/$service"
if [[ -d "$service_dir" ]]; then
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find "$service_dir" -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.json" -o -name "*.conf" -o -name "*.cfg" -o -name "*.env" \) -print0 2>/dev/null)
else
print_error "Service directory not found: $service_dir"
return 1
fi
else
# Process all services
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find "$EZ_HOME/docker-compose" -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.json" -o -name "*.conf" -o -name "*.cfg" -o -name "*.env" \) -print0 2>/dev/null)
fi
printf '%s\n' "${files[@]}"
}
# Check if file contains template variables
file_has_templates() {
local file="$1"
for var in "${TEMPLATE_VARS[@]}"; do
if grep -q "\${$var}" "$file" 2>/dev/null; then
return 0
fi
done
return 1
}
# Process template file
process_template_file() {
local file="$1"
local backup="${file}.template"
print_info "Processing: $file"
# Check if file has templates
if ! file_has_templates "$file"; then
print_info "No templates found in $file"
return 0
fi
# Backup original if not already backed up
if [[ ! -f "$backup" ]]; then
cp "$file" "$backup"
print_info "Backed up original to $backup"
fi
# Process template variables
local temp_file
temp_file=$(mktemp)
cp "$file" "$temp_file"
for var in "${TEMPLATE_VARS[@]}"; do
local value="${!var:-}"
if [[ -n "$value" ]]; then
# Use sed to replace ${VAR} with value
sed -i "s|\${$var}|$value|g" "$temp_file"
log_debug "Replaced \${$var} with $value in $file"
else
log_warn "Variable $var not set, leaving template as-is"
fi
done
# Move processed file back
mv "$temp_file" "$file"
print_success "Processed $file"
return 0
}
# Validate processed files
validate_processed_files() {
local files=("$@")
local errors=0
print_info "Validating processed files..."
for file in "${files[@]}"; do
if [[ "$file" =~ \.(yml|yaml)$ ]]; then
if ! validate_yaml "$file"; then
print_error "Invalid YAML in $file"
errors=$((errors + 1))
fi
fi
done
if [[ $errors -gt 0 ]]; then
print_error "Validation failed for $errors file(s)"
return 1
fi
print_success "All files validated successfully"
return 0
}
# =============================================================================
# UI FUNCTIONS
# =============================================================================
# Show progress for batch processing
show_localization_progress() {
local total="$1"
local current="$2"
local file="$3"
local percent=$(( current * 100 / total ))
ui_gauge "Processing templates... ($current/$total)" "$percent"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local service=""
local non_interactive=false
local verbose=false
local dry_run=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << EOF
EZ-Homelab Configuration Localization
USAGE:
$SCRIPT_NAME [OPTIONS] [SERVICE]
ARGUMENTS:
SERVICE Specific service to localize (optional, processes all if not specified)
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose logging
--dry-run Show what would be processed without making changes
--no-ui Run without interactive UI
EXAMPLES:
$SCRIPT_NAME # Process all services
$SCRIPT_NAME traefik # Process only Traefik
$SCRIPT_NAME --dry-run # Show what would be changed
EOF
exit 0
;;
-v|--verbose)
verbose=true
;;
--dry-run)
dry_run=true
;;
--no-ui)
non_interactive=true
;;
-*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
*)
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Multiple services specified. Use only one service name."
exit 1
fi
;;
esac
shift
done
# Initialize script
init_script "$SCRIPT_NAME"
if $verbose; then
set -x
fi
print_info "Starting EZ-Homelab configuration localization..."
# Load environment
if ! load_environment; then
exit 1
fi
# Find template files
local files
mapfile -t files < <(find_template_files "$service")
if [[ ${#files[@]} -eq 0 ]]; then
print_warning "No template files found"
exit 0
fi
print_info "Found ${#files[@]} template file(s) to process"
if $dry_run; then
print_info "DRY RUN - Would process the following files:"
printf '%s\n' "${files[@]}"
exit 0
fi
# Process files
local processed=0
local total=${#files[@]}
for file in "${files[@]}"; do
if ui_available && ! $non_interactive; then
show_localization_progress "$total" "$processed" "$file"
fi
if process_template_file "$file"; then
processed=$((processed + 1))
fi
done
# Close progress gauge
if ui_available && ! $non_interactive; then
ui_gauge "Processing complete!" 100
sleep 1
fi
# Validate processed files
if ! validate_processed_files "${files[@]}"; then
print_error "Some processed files failed validation"
print_error "Check the log file: $LOG_FILE"
exit 1
fi
echo ""
print_success "Configuration localization complete!"
print_info "Processed $processed file(s)"
print_info "Templates backed up with .template extension"
print_info "Next step: ./validate.sh"
exit 0
}
# Run main function
main "$@"

577
scripts/enhanced-setup/monitor.sh Executable file
View File

@@ -0,0 +1,577 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Service Monitoring
# Real-time service monitoring and alerting
SCRIPT_NAME="monitor"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# MONITORING CONFIGURATION
# =============================================================================
# Monitoring intervals (seconds)
HEALTH_CHECK_INTERVAL=30
RESOURCE_CHECK_INTERVAL=60
LOG_CHECK_INTERVAL=300
# Alert thresholds
CPU_THRESHOLD=80
MEMORY_THRESHOLD=80
DISK_THRESHOLD=90
# Alert cooldown (seconds) - prevent alert spam
ALERT_COOLDOWN=300
# Monitoring state file
MONITOR_STATE_FILE="$LOG_DIR/monitor_state.json"
# =============================================================================
# MONITORING STATE MANAGEMENT
# =============================================================================
# Initialize monitoring state
init_monitor_state() {
if [[ ! -f "$MONITOR_STATE_FILE" ]]; then
cat > "$MONITOR_STATE_FILE" << EOF
{
"services": {},
"alerts": {},
"last_check": $(date +%s),
"system_stats": {}
}
EOF
fi
}
# Update service state
update_service_state() {
local service="$1"
local status="$2"
local timestamp
timestamp=$(date +%s)
# Use jq if available, otherwise use sed
if command_exists "jq"; then
jq --arg service "$service" --arg status "$status" --argjson timestamp "$timestamp" \
'.services[$service] = {"status": $status, "last_update": $timestamp}' \
"$MONITOR_STATE_FILE" > "${MONITOR_STATE_FILE}.tmp" && mv "${MONITOR_STATE_FILE}.tmp" "$MONITOR_STATE_FILE"
else
# Simple fallback without jq
log_warn "jq not available, using basic state tracking"
fi
}
# Check if alert should be sent (cooldown check)
should_alert() {
local alert_key="$1"
local current_time
current_time=$(date +%s)
if command_exists "jq"; then
local last_alert
last_alert=$(jq -r ".alerts[\"$alert_key\"] // 0" "$MONITOR_STATE_FILE")
local time_diff=$((current_time - last_alert))
if (( time_diff >= ALERT_COOLDOWN )); then
# Update last alert time
jq --arg alert_key "$alert_key" --argjson timestamp "$current_time" \
'.alerts[$alert_key] = $timestamp' \
"$MONITOR_STATE_FILE" > "${MONITOR_STATE_FILE}.tmp" && mv "${MONITOR_STATE_FILE}.tmp" "$MONITOR_STATE_FILE"
return 0
else
return 1
fi
else
# Without jq, always alert (no cooldown)
return 0
fi
}
# =============================================================================
# HEALTH MONITORING FUNCTIONS
# =============================================================================
# Check service health
check_service_health() {
local service="$1"
if ! is_service_running "$service"; then
if should_alert "service_down_$service"; then
print_error "ALERT: Service '$service' is down"
log_error "Service '$service' is down"
fi
update_service_state "$service" "down"
return 1
fi
# Check container health status
local health_status
health_status=$(docker inspect "$service" --format '{{.State.Health.Status}}' 2>/dev/null || echo "unknown")
case "$health_status" in
"healthy")
update_service_state "$service" "healthy"
;;
"unhealthy")
if should_alert "service_unhealthy_$service"; then
print_warning "ALERT: Service '$service' is unhealthy"
log_warn "Service '$service' is unhealthy"
fi
update_service_state "$service" "unhealthy"
return 1
;;
"starting")
update_service_state "$service" "starting"
;;
*)
update_service_state "$service" "unknown"
;;
esac
return 0
}
# Check all services health
check_all_services_health() {
print_info "Checking service health..."
local services
mapfile -t services < <(find_all_services)
local unhealthy_count=0
for service in "${services[@]}"; do
if ! check_service_health "$service"; then
((unhealthy_count++))
fi
done
if (( unhealthy_count == 0 )); then
print_success "All services are healthy"
else
print_warning "$unhealthy_count service(s) have issues"
fi
}
# =============================================================================
# RESOURCE MONITORING FUNCTIONS
# =============================================================================
# Check system resources
check_system_resources() {
print_info "Checking system resources..."
# CPU usage
local cpu_usage
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
cpu_usage=$(printf "%.0f" "$cpu_usage")
if (( cpu_usage > CPU_THRESHOLD )); then
if should_alert "high_cpu"; then
print_error "ALERT: High CPU usage: ${cpu_usage}% (threshold: ${CPU_THRESHOLD}%)"
log_error "High CPU usage: ${cpu_usage}%"
fi
fi
# Memory usage
local memory_usage
memory_usage=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100.0}')
if (( memory_usage > MEMORY_THRESHOLD )); then
if should_alert "high_memory"; then
print_error "ALERT: High memory usage: ${memory_usage}% (threshold: ${MEMORY_THRESHOLD}%)"
log_error "High memory usage: ${memory_usage}%"
fi
fi
# Disk usage
local disk_usage
disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if (( disk_usage > DISK_THRESHOLD )); then
if should_alert "high_disk"; then
print_error "ALERT: High disk usage: ${disk_usage}% (threshold: ${DISK_THRESHOLD}%)"
log_error "High disk usage: ${disk_usage}%"
fi
fi
print_info "CPU: ${cpu_usage}%, Memory: ${memory_usage}%, Disk: ${disk_usage}%"
}
# Check Docker resource usage
check_docker_resources() {
print_info "Checking Docker resources..."
# Get container resource usage
if command_exists "docker" && docker_available; then
local containers
mapfile -t containers < <(docker ps --format "{{.Names}}")
for container in "${containers[@]}"; do
local stats
stats=$(docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemPerc}}" "$container" 2>/dev/null | tail -n 1)
if [[ -n "$stats" ]]; then
local cpu_perc mem_perc
cpu_perc=$(echo "$stats" | awk '{print $2}' | sed 's/%//')
mem_perc=$(echo "$stats" | awk '{print $3}' | sed 's/%//')
# Convert to numbers for comparison
cpu_perc=${cpu_perc%.*}
mem_perc=${mem_perc%.*}
if [[ "$cpu_perc" =~ ^[0-9]+$ ]] && (( cpu_perc > CPU_THRESHOLD )); then
if should_alert "container_high_cpu_$container"; then
print_warning "ALERT: Container '$container' high CPU: ${cpu_perc}%"
log_warn "Container '$container' high CPU: ${cpu_perc}%"
fi
fi
if [[ "$mem_perc" =~ ^[0-9]+$ ]] && (( mem_perc > MEMORY_THRESHOLD )); then
if should_alert "container_high_memory_$container"; then
print_warning "ALERT: Container '$container' high memory: ${mem_perc}%"
log_warn "Container '$container' high memory: ${mem_perc}%"
fi
fi
fi
done
fi
}
# =============================================================================
# LOG MONITORING FUNCTIONS
# =============================================================================
# Check service logs for errors
check_service_logs() {
local service="$1"
local since="${2:-1m}" # Default to last minute
if ! is_service_running "$service"; then
return 0
fi
local compose_file
compose_file=$(get_service_compose_file "$service")
if [[ -z "$compose_file" ]]; then
return 1
fi
local compose_dir=$(dirname "$compose_file")
local compose_base=$(basename "$compose_file")
# Check for error patterns in recent logs
local error_patterns=("ERROR" "error" "Exception" "failed" "Failed" "panic" "PANIC")
local errors_found=()
for pattern in "${error_patterns[@]}"; do
local error_count
error_count=$(cd "$compose_dir" && docker compose logs --since="$since" "$service" 2>&1 | grep -c "$pattern" || true)
if (( error_count > 0 )); then
errors_found+=("$pattern: $error_count")
fi
done
if [[ ${#errors_found[@]} -gt 0 ]]; then
if should_alert "log_errors_$service"; then
print_warning "ALERT: Service '$service' has errors in logs: ${errors_found[*]}"
log_warn "Service '$service' log errors: ${errors_found[*]}"
fi
fi
}
# Check all services logs
check_all_logs() {
print_info "Checking service logs for errors..."
local services
mapfile -t services < <(find_all_services)
for service in "${services[@]}"; do
check_service_logs "$service"
done
}
# =============================================================================
# MONITORING DISPLAY FUNCTIONS
# =============================================================================
# Display monitoring dashboard
show_monitoring_dashboard() {
print_info "EZ-Homelab Monitoring Dashboard"
echo
# System resources
echo "=== System Resources ==="
local cpu_usage memory_usage disk_usage
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}' || echo "0")
memory_usage=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100.0}' || echo "0")
disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//' || echo "0")
echo "CPU Usage: ${cpu_usage}%"
echo "Memory Usage: ${memory_usage}%"
echo "Disk Usage: ${disk_usage}%"
echo
# Service status summary
echo "=== Service Status ==="
local services=()
mapfile -t services < <(find_all_services)
local total_services=${#services[@]}
local running_services=0
local unhealthy_services=0
for service in "${services[@]}"; do
if is_service_running "$service"; then
running_services=$((running_services + 1))
local health_status
health_status=$(docker inspect "$service" --format '{{.State.Health.Status}}' 2>/dev/null || echo "unknown")
if [[ "$health_status" == "unhealthy" ]]; then
unhealthy_services=$((unhealthy_services + 1))
fi
fi
done
echo "Total Services: $total_services"
echo "Running: $running_services"
echo "Unhealthy: $unhealthy_services"
echo
# Recent alerts
echo "=== Recent Alerts ==="
if command_exists "jq" && [[ -f "$MONITOR_STATE_FILE" ]]; then
local recent_alerts
recent_alerts=$(jq -r '.alerts | to_entries[] | select(.value > (now - 3600)) | "\(.key): \(.value | strftime("%H:%M:%S"))"' "$MONITOR_STATE_FILE" 2>/dev/null || echo "")
if [[ -n "$recent_alerts" ]]; then
echo "$recent_alerts"
else
echo "No recent alerts (last hour)"
fi
else
echo "Alert history not available (jq not installed)"
fi
}
# Display detailed service status
show_detailed_status() {
local service="$1"
if [[ -z "$service" ]]; then
print_error "Service name required"
return 1
fi
print_info "Detailed Status for: $service"
echo
if ! is_service_running "$service"; then
echo "Status: ❌ Stopped"
return 0
fi
echo "Status: ✅ Running"
# Container details
local container_info
container_info=$(docker ps --filter "name=^${service}$" --format "table {{.Image}}\t{{.Status}}\t{{.Ports}}" | tail -n +2)
if [[ -n "$container_info" ]]; then
echo "Container: $container_info"
fi
# Health status
local health_status
health_status=$(docker inspect "$service" --format '{{.State.Health.Status}}' 2>/dev/null || echo "N/A")
echo "Health: $health_status"
# Resource usage
local stats
stats=$(docker stats --no-stream --format "table {{.CPUPerc}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}" "$service" 2>/dev/null | tail -n +2)
if [[ -n "$stats" ]]; then
echo "Resources: $stats"
fi
# Recent logs
echo
echo "Recent Logs:"
local compose_file
compose_file=$(get_service_compose_file "$service")
if [[ -n "$compose_file" ]]; then
local compose_dir=$(dirname "$compose_file")
local compose_base=$(basename "$compose_file")
(cd "$compose_dir" && docker compose logs --tail=5 "$service" 2>/dev/null || echo "No logs available")
fi
}
# =============================================================================
# CONTINUOUS MONITORING FUNCTIONS
# =============================================================================
# Run continuous monitoring
run_continuous_monitoring() {
local interval="${1:-$HEALTH_CHECK_INTERVAL}"
print_info "Starting continuous monitoring (interval: ${interval}s)"
print_info "Press Ctrl+C to stop"
# Initialize state
init_monitor_state
# Main monitoring loop
while true; do
local start_time
start_time=$(date +%s)
# Run all checks
check_all_services_health
check_system_resources
check_docker_resources
check_all_logs
# Update timestamp
if command_exists "jq"; then
jq --argjson timestamp "$(date +%s)" '.last_check = $timestamp' \
"$MONITOR_STATE_FILE" > "${MONITOR_STATE_FILE}.tmp" && mv "${MONITOR_STATE_FILE}.tmp" "$MONITOR_STATE_FILE"
fi
local end_time
end_time=$(date +%s)
local duration=$((end_time - start_time))
print_info "Monitoring cycle completed in ${duration}s. Next check in $((interval - duration))s..."
# Sleep for remaining time
local sleep_time=$((interval - duration))
if (( sleep_time > 0 )); then
sleep "$sleep_time"
fi
done
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local action=""
local service=""
local interval="$HEALTH_CHECK_INTERVAL"
local continuous=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << EOF
EZ-Homelab Service Monitoring
USAGE:
monitor [OPTIONS] <ACTION> [SERVICE]
ACTIONS:
dashboard Show monitoring dashboard
status Show detailed status for a service
check Run all monitoring checks once
watch Continuous monitoring mode
OPTIONS:
-i, --interval SEC Monitoring interval in seconds (default: $HEALTH_CHECK_INTERVAL)
-c, --continuous Run in continuous mode (same as 'watch')
EXAMPLES:
monitor dashboard # Show monitoring dashboard
monitor status traefik # Show detailed status for Traefik
monitor check # Run all checks once
monitor watch # Start continuous monitoring
monitor watch -i 60 # Continuous monitoring every 60 seconds
EOF
exit 0
;;
-i|--interval)
interval="$2"
shift 2
;;
-c|--continuous)
continuous=true
shift
;;
dashboard|status|check|watch)
action="$1"
shift
break
;;
*)
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Too many arguments"
exit 1
fi
shift
;;
esac
done
# Handle remaining arguments
while [[ $# -gt 0 ]]; do
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Too many arguments"
exit 1
fi
shift
done
# Initialize script
init_script "$SCRIPT_NAME" "$SCRIPT_VERSION"
init_logging "$SCRIPT_NAME"
init_monitor_state
# Check prerequisites
if ! docker_available; then
print_error "Docker is not available"
exit 1
fi
# Execute action
case "$action" in
dashboard)
show_monitoring_dashboard
;;
status)
if [[ -n "$service" ]]; then
show_detailed_status "$service"
else
print_error "Service name required for status action"
exit 1
fi
;;
check)
check_all_services_health
check_system_resources
check_docker_resources
check_all_logs
;;
watch)
run_continuous_monitoring "$interval"
;;
"")
# Default action: show dashboard
show_monitoring_dashboard
;;
*)
print_error "Unknown action: $action"
exit 1
;;
esac
}
# Run main function
main "$@"

View File

@@ -0,0 +1,352 @@
# EZ-Homelab Enhanced Setup Scripts - Product Requirements Document
## Document Information
- **Project**: EZ-Homelab Enhanced Setup Scripts
- **Version**: 1.0
- **Date**: January 29, 2026
- **Author**: EZ-Homelab Development Team
- **Location**: `scripts/enhanced-setup/`
## Executive Summary
The EZ-Homelab Enhanced Setup Scripts project aims to replace the complex Python TUI deployment system with a modular, bash-based suite of scripts that provide automated, user-friendly deployment of the EZ-Homelab infrastructure. This approach prioritizes simplicity, minimal manual intervention, and cross-architecture compatibility (AMD64/ARM64) while maintaining the project's file-based, AI-manageable architecture.
The solution consists of 11 specialized scripts that handle different aspects of homelab deployment, from pre-flight checks to ongoing management and monitoring.
## Objectives
### Primary Objectives
- **Simplify Deployment**: Reduce manual steps for inexperienced users to near-zero
- **Cross-Platform Support**: Ensure seamless operation on AMD64 and ARM64 architectures
- **Modular Design**: Create reusable, focused scripts instead of monolithic solutions
- **Error Resilience**: Provide clear error messages and recovery options
- **Maintainability**: Keep code AI-manageable and file-based
### Secondary Objectives
- **User Experience**: Implement text-based UI with dynamic menus using dialog/whiptail
- **Automation**: Support both interactive and non-interactive (scripted) execution
- **Monitoring**: Provide status reporting tools for ongoing management
- **Security**: Maintain security-first principles with proper permission handling
## Target Users
### Primary Users
- **Inexperienced Homelab Enthusiasts**: Users new to Docker/homelab concepts
- **Raspberry Pi Users**: ARM64 users with resource constraints
- **Single-Server Deployers**: Users setting up complete homelabs on one machine
### Secondary Users
- **Advanced Users**: Those who want granular control over deployment
- **Multi-Server Administrators**: Users managing distributed homelab setups
- **Developers**: Contributors to EZ-Homelab who need to test changes
## Requirements
### Functional Requirements
#### FR-1: Pre-Flight System Validation (`preflight.sh`)
- **Description**: Perform comprehensive system checks before deployment
- **Requirements**:
- Check OS compatibility (Debian/Ubuntu-based systems)
- Verify architecture support (AMD64/ARM64)
- Assess available disk space (minimum 20GB for core deployment)
- Check network connectivity and DNS resolution
- Validate CPU and memory resources
- Detect existing Docker installation
- Check for NVIDIA GPU presence
- **Output**: Detailed report with pass/fail status and recommendations
- **UI**: Progress bar with whiptail/dialog
#### FR-2: System Setup and Prerequisites (`setup.sh`)
- **Description**: Install and configure Docker and system prerequisites
- **Requirements**:
- Install Docker Engine (version 24.0+)
- Configure Docker daemon for Traefik
- Add user to docker group
- Install required system packages (curl, jq, git)
- Set up virtual environments for Python dependencies (ARM64 compatibility)
- Handle system reboot requirements gracefully
- **Output**: Installation log with success confirmation
- **UI**: Progress indicators and user prompts for reboots
#### FR-3: NVIDIA GPU Setup (`nvidia.sh`)
- **Description**: Install NVIDIA drivers and configure GPU support
- **Requirements**:
- Detect NVIDIA GPU presence
- Install official NVIDIA drivers (version 525+ for current GPUs)
- Configure Docker NVIDIA runtime
- Validate GPU functionality with nvidia-smi
- Handle driver conflicts and updates
- **Output**: GPU detection and installation status
- **UI**: Confirmation prompts and progress tracking
#### FR-4: Pre-Deployment Configuration Wizard (`pre-deployment-wizard.sh`)
- **Description**: Interactive setup of deployment options and environment
- **Requirements**:
- Create required Docker networks (traefik-network, homelab-network)
- Guide user through deployment type selection (Core, Single Server, Remote)
- Service selection with checkboxes (dynamic based on deployment type)
- Environment variable collection (.env file creation)
- Domain configuration (DuckDNS setup)
- Architecture-specific option handling
- **Output**: Generated .env file and network configurations
- **UI**: Dynamic dialog menus with conditional questions
#### FR-5: Multi-Purpose Validation (`validate.sh`)
- **Description**: Validate configurations, compose files, and deployment readiness
- **Requirements**:
- Validate .env file completeness and syntax
- Check Docker Compose file syntax (`docker compose config`)
- Verify network availability
- Validate service dependencies
- Check SSL certificate readiness
- Perform architecture-specific validations
- **Output**: Validation report with error details and fixes
- **UI**: Optional progress display, detailed error messages
#### FR-6: Configuration Localization (`localize.sh`)
- **Description**: Replace template variables in service configurations
- **Requirements**:
- Process per-service configuration files
- Replace ${VARIABLE} placeholders with environment values
- Handle nested configurations (YAML, JSON, conf files)
- Support selective localization (single service or all)
- Preserve original templates for generalization
- **Output**: Localized configuration files ready for deployment
- **UI**: Progress for batch operations
#### FR-7: Configuration Generalization (`generalize.sh`)
- **Description**: Reverse localization for template maintenance
- **Requirements**:
- Extract environment values back to ${VARIABLE} format
- Update template files from localized versions
- Support selective generalization
- Maintain configuration integrity
- **Output**: Updated template files
- **UI**: Confirmation prompts for destructive operations
#### FR-8: Service Deployment (`deploy.sh`)
- **Description**: Deploy single stacks or complete homelab
- **Requirements**:
- Support deployment of individual services/stacks
- Enforce deployment order (core first, then others)
- Handle service dependencies and health checks
- Provide rollback options for failed deployments
- Support both interactive and automated modes
- Log deployment progress and errors
- **Output**: Deployment status and access URLs
- **UI**: Progress bars and real-time status updates
#### FR-9: Uninstall and Cleanup (`uninstall.sh`)
- **Description**: Remove services, stacks, or complete homelab
- **Requirements**:
- Support selective uninstall (service, stack, or full)
- Preserve user data with confirmation
- Clean up Docker networks and volumes
- Remove generated configurations
- Provide safety confirmations
- **Output**: Cleanup report with remaining resources
- **UI**: Confirmation dialogs and progress tracking
#### FR-10: Proxy Configuration Status (`proxy-status.sh`)
- **Description**: Generate comprehensive proxy configuration report
- **Requirements**:
- Analyze Docker Compose labels for Traefik routing
- Check external host configurations in Traefik dynamic files
- Validate Sablier lazy loading configurations
- Support local and remote server analysis
- Include all stacks (deployed and not deployed)
- Generate table-format reports
- **Output**: HTML/PDF report with configuration status
- **UI**: Table display with color-coded status
#### FR-11: DNS and SSL Status (`dns-status.sh`)
- **Description**: Report on DuckDNS and Let's Encrypt certificate status
- **Requirements**:
- Check DuckDNS subdomain resolution
- Validate SSL certificate validity and expiration
- Monitor certificate renewal status
- Report on DNS propagation
- Include wildcard certificate coverage
- **Output**: Certificate and DNS health report
- **UI**: Status dashboard with alerts
### Non-Functional Requirements
#### NFR-1: Performance
- **Startup Time**: Scripts should complete pre-flight checks in <30 seconds
- **Deployment Time**: Core services deployment in <5 minutes on standard hardware
- **Memory Usage**: <100MB RAM for script execution
- **Disk Usage**: <500MB for script and temporary files
#### NFR-2: Reliability
- **Error Recovery**: Scripts should handle common failures gracefully
- **Idempotency**: Safe to re-run scripts without side effects
- **Logging**: Comprehensive logging to `/var/log/ez-homelab/`
- **Backup**: Automatic backup of configurations before modifications
#### NFR-3: Usability
- **User Guidance**: Clear error messages with suggested fixes
- **Documentation**: Inline help (`--help`) for all scripts
- **Localization**: English language with clear technical terms
- **Accessibility**: Keyboard-only navigation for text UI
#### NFR-4: Security
- **Permission Handling**: Proper sudo usage with minimal privilege escalation
- **Secret Management**: Secure handling of passwords and API keys
- **Network Security**: No unnecessary port exposures during setup
- **Audit Trail**: Log all configuration changes
#### NFR-5: Compatibility
- **OS Support**: Debian 11+, Ubuntu 20.04+, Raspberry Pi OS
- **Architecture**: AMD64 and ARM64
- **Docker**: Version 20.10+ with Compose V2
- **Dependencies**: Use only widely available packages
## Technical Specifications
### Software Dependencies
- **Core System**:
- bash 5.0+
- curl 7.68+
- jq 1.6+
- git 2.25+
- dialog 1.3+ (or whiptail 0.52+)
- **Docker Ecosystem**:
- Docker Engine 24.0+
- Docker Compose V2 (docker compose plugin)
- Docker Buildx for multi-architecture builds
- **NVIDIA (Optional)**:
- NVIDIA Driver 525+
- nvidia-docker2 2.12+
- **Python (Virtual Environment)**:
- Python 3.9+
- pip 21.0+
- virtualenv 20.0+
### Architecture Considerations
- **AMD64**: Full feature support, optimized performance
- **ARM64**: PiWheels integration, resource-aware deployment
- **Multi-Server**: TLS certificate management for remote access
### File Structure
```
scripts/enhanced-setup/
├── prd.md # This document
├── preflight.sh # System validation
├── setup.sh # Docker installation
├── nvidia.sh # GPU setup
├── pre-deployment-wizard.sh # Configuration wizard
├── validate.sh # Multi-purpose validation
├── localize.sh # Template processing
├── generalize.sh # Template reversal
├── deploy.sh # Service deployment
├── uninstall.sh # Cleanup operations
├── proxy-status.sh # Proxy configuration report
├── dns-status.sh # DNS/SSL status report
├── lib/ # Shared functions
│ ├── common.sh # Utility functions
│ ├── ui.sh # Dialog/whiptail helpers
│ └── validation.sh # Validation logic
├── templates/ # Configuration templates
└── logs/ # Execution logs
```
### Integration Points
- **EZ-Homelab Repository**: Located in `~/EZ-Homelab/`
- **Runtime Location**: Deploys to `/opt/stacks/`
- **Configuration Source**: Uses `.env` files and templates
- **Service Definitions**: Leverages existing `docker-compose/` directory
## User Stories
### US-1: First-Time Raspberry Pi Setup
**As a** Raspberry Pi user new to homelabs
**I want** a guided setup process
**So that** I can deploy EZ-Homelab without Docker knowledge
**Acceptance Criteria**:
- Pre-flight detects ARM64 and guides Pi-specific setup
- Setup script handles Docker installation on Raspbian
- Wizard provides Pi-optimized service selections
- Deployment completes without manual intervention
### US-2: Multi-Server Homelab Administrator
**As a** homelab administrator with multiple servers
**I want** to deploy services across servers
**So that** I can manage distributed infrastructure
**Acceptance Criteria**:
- Proxy-status reports configuration across all servers
- Deploy script supports remote server targeting
- DNS-status validates certificates for all subdomains
- Uninstall handles cross-server cleanup
### US-3: Development and Testing
**As a** developer contributing to EZ-Homelab
**I want** to validate changes before deployment
**So that** I can ensure quality and compatibility
**Acceptance Criteria**:
- Validate script checks all configurations
- Localize/generalize supports template development
- Deploy script allows single-service testing
- Status scripts provide detailed diagnostic information
## Implementation Plan
### Phase 1: Core Infrastructure (Week 1-2)
- Implement preflight.sh and setup.sh
- Create shared library functions
- Set up basic dialog UI framework
### Phase 2: Configuration Management (Week 3-4)
- Build pre-deployment-wizard.sh
- Implement localize.sh and generalize.sh
- Add validation.sh framework
### Phase 3: Deployment Engine (Week 5-6)
- Create deploy.sh with service orchestration
- Implement uninstall.sh
- Add comprehensive error handling
### Phase 4: Monitoring and Reporting (Week 7-8)
- Build proxy-status.sh and dns-status.sh
- Add nvidia.sh for GPU support
- Comprehensive testing across architectures
### Phase 5: Polish and Documentation (Week 9-10)
- UI/UX improvements
- Documentation and help systems
- Performance optimization
## Risk Assessment
### Technical Risks
- **ARM64 Compatibility**: Mitigated by early testing on Raspberry Pi
- **Dialog/Whiptail Availability**: Low risk - included in Debian/Ubuntu
- **Docker API Changes**: Mitigated by using stable Docker versions
### Operational Risks
- **User Adoption**: Addressed through clear documentation and UI
- **Maintenance Overhead**: Mitigated by modular design
- **Security Vulnerabilities**: Addressed through regular updates and audits
## Success Metrics
### Quantitative Metrics
- **Deployment Success Rate**: >95% first-time success
- **Setup Time**: <15 minutes for basic deployment
- **Error Rate**: <5% user-reported issues
- **Architecture Coverage**: Full AMD64/ARM64 support
### Qualitative Metrics
- **User Satisfaction**: Positive feedback on simplicity
- **Community Adoption**: Increased GitHub stars and contributors
- **Maintainability**: Easy to add new services and features
## Conclusion
The EZ-Homelab Enhanced Setup Scripts project will provide a robust, user-friendly deployment system that addresses the limitations of the previous Python approach while maintaining the project's core principles of simplicity and automation. The modular script design ensures maintainability and extensibility for future homelab needs.
This PRD serves as the foundation for implementation and will be updated as development progresses.

View File

@@ -0,0 +1,372 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Pre-Deployment Configuration Wizard
# Interactive setup of deployment options and environment configuration
SCRIPT_NAME="pre-deployment-wizard"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# SCRIPT CONFIGURATION
# =============================================================================
# Default values
DEFAULT_DOMAIN="example.duckdns.org"
DEFAULT_TIMEZONE="America/New_York"
DEFAULT_PUID=1000
DEFAULT_PGID=1000
# Service stacks
CORE_STACKS=("duckdns" "traefik" "authelia" "gluetun" "sablier")
INFRA_STACKS=("dockge" "pihole")
DASHBOARD_STACKS=("homepage" "homarr")
MEDIA_STACKS=("plex" "jellyfin" "calibre-web" "qbittorrent")
MEDIA_MGMT_STACKS=("sonarr" "radarr" "bazarr" "lidarr" "readarr" "prowlarr")
HOME_STACKS=("homeassistant" "nodered" "zigbee2mqtt")
PRODUCTIVITY_STACKS=("nextcloud" "gitea" "bookstack")
MONITORING_STACKS=("grafana" "prometheus" "uptimekuma")
UTILITY_STACKS=("duplicati" "freshrss" "wallabag")
# =============================================================================
# CONFIGURATION FUNCTIONS
# =============================================================================
# Create required Docker networks
create_docker_networks() {
print_info "Creating required Docker networks..."
local networks=("traefik-network" "homelab-network")
for network in "${networks[@]}"; do
if ! docker network ls --format "{{.Name}}" | grep -q "^${network}$"; then
docker network create "$network" || {
print_error "Failed to create network: $network"
return 1
}
print_success "Created network: $network"
else
print_info "Network already exists: $network"
fi
done
return 0
}
# Generate .env file
generate_env_file() {
local domain="$1"
local timezone="$2"
local puid="$3"
local pgid="$4"
local deployment_type="$5"
print_info "Generating .env file..."
local env_file="$EZ_HOME/.env"
local temp_file
temp_file=$(mktemp)
# Generate secrets
local jwt_secret
jwt_secret=$(openssl rand -hex 64 2>/dev/null || echo "change-me-jwt-secret")
local session_secret
session_secret=$(openssl rand -hex 64 2>/dev/null || echo "change-me-session-secret")
local encryption_key
encryption_key=$(openssl rand -hex 64 2>/dev/null || echo "change-me-encryption-key")
local duckdns_token="your-duckdns-token-here"
# Write environment variables
cat > "$temp_file" << EOF
# EZ-Homelab Environment Configuration
# Generated by pre-deployment-wizard.sh on $(date)
# Domain and Networking
DOMAIN=$domain
TZ=$timezone
PUID=$puid
PGID=$pgid
# DuckDNS Configuration
DUCKDNS_TOKEN=$duckdns_token
# Authelia Secrets (Change these in production!)
JWT_SECRET=$jwt_secret
SESSION_SECRET=$session_secret
ENCRYPTION_KEY=$encryption_key
# Deployment Configuration
DEPLOYMENT_TYPE=$deployment_type
SERVER_HOSTNAME=$(hostname)
# Docker Configuration
DOCKER_SOCKET_PATH=/var/run/docker.sock
# Default Credentials (Change these!)
AUTHELIA_ADMIN_PASSWORD=admin
AUTHELIA_ADMIN_EMAIL=admin@example.com
# Service-specific settings
PLEX_CLAIM_TOKEN=your-plex-claim-token
EOF
# Backup existing .env if it exists
if [[ -f "$env_file" ]]; then
backup_file "$env_file"
fi
# Move to final location
mv "$temp_file" "$env_file"
chmod 600 "$env_file"
print_success ".env file created at $env_file"
print_warning "IMPORTANT: Edit the .env file to set your actual secrets and tokens!"
}
# Get service selection based on deployment type
get_service_selection() {
local deployment_type="$1"
local selected_services=()
case "$deployment_type" in
"core")
selected_services=("${CORE_STACKS[@]}")
;;
"single")
# Show all categories for single server
local all_services=(
"${CORE_STACKS[@]}"
"${INFRA_STACKS[@]}"
"${DASHBOARD_STACKS[@]}"
"${MEDIA_STACKS[@]}"
"${MEDIA_MGMT_STACKS[@]}"
"${HOME_STACKS[@]}"
"${PRODUCTIVITY_STACKS[@]}"
"${MONITORING_STACKS[@]}"
"${UTILITY_STACKS[@]}"
)
selected_services=("${all_services[@]}")
;;
"remote")
# Remote servers get infrastructure + services (no core)
local remote_services=(
"${INFRA_STACKS[@]}"
"${DASHBOARD_STACKS[@]}"
"${MEDIA_STACKS[@]}"
"${MEDIA_MGMT_STACKS[@]}"
"${HOME_STACKS[@]}"
"${PRODUCTIVITY_STACKS[@]}"
"${MONITORING_STACKS[@]}"
"${UTILITY_STACKS[@]}"
)
selected_services=("${remote_services[@]}")
;;
esac
echo "${selected_services[@]}"
}
# Validate configuration
validate_configuration() {
local domain="$1"
local deployment_type="$2"
print_info "Validating configuration..."
# Validate domain format
if [[ ! "$domain" =~ ^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
print_error "Invalid domain format: $domain"
return 1
fi
# Validate deployment type
case "$deployment_type" in
"core"|"single"|"remote")
;;
*)
print_error "Invalid deployment type: $deployment_type"
return 1
;;
esac
print_success "Configuration validation passed"
return 0
}
# =============================================================================
# UI FUNCTIONS
# =============================================================================
# Show welcome screen
show_welcome() {
ui_msgbox "Welcome to EZ-Homelab Setup Wizard!
This wizard will help you configure your EZ-Homelab deployment by:
• Setting up Docker networks
• Configuring environment variables
• Selecting services to deploy
• Generating configuration files
Press OK to continue or ESC to cancel."
}
# Get domain configuration
get_domain_config() {
local domain
domain=$(ui_inputbox "Enter your domain (e.g., yourname.duckdns.org):" "$DEFAULT_DOMAIN")
[[ -z "$domain" ]] && return 1
echo "$domain"
}
# Get timezone
get_timezone() {
local timezone
timezone=$(ui_inputbox "Enter your timezone (e.g., America/New_York):" "$DEFAULT_TIMEZONE")
[[ -z "$timezone" ]] && return 1
echo "$timezone"
}
# Get PUID/PGID
get_user_ids() {
local puid pgid
puid=$(ui_inputbox "Enter PUID (User ID for Docker containers):" "$DEFAULT_PUID")
[[ -z "$puid" ]] && return 1
pgid=$(ui_inputbox "Enter PGID (Group ID for Docker containers):" "$DEFAULT_PGID")
[[ -z "$pgid" ]] && return 1
echo "$puid $pgid"
}
# Get deployment type
get_deployment_type() {
ui_select_deployment_type
}
# Confirm configuration
confirm_configuration() {
local domain="$1"
local timezone="$2"
local puid="$3"
local pgid="$4"
local deployment_type="$5"
local services="$6"
local message="Configuration Summary:
Domain: $domain
Timezone: $timezone
PUID/PGID: $puid/$pgid
Deployment Type: $deployment_type
Services: $services
Do you want to proceed with this configuration?"
ui_yesno "$message"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local non_interactive=false
local verbose=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
ui_show_help "$SCRIPT_NAME"
exit 0
;;
--no-ui)
non_interactive=true
;;
-v|--verbose)
verbose=true
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
shift
done
# Initialize script
init_script "$SCRIPT_NAME"
if $verbose; then
set -x
fi
print_info "Starting EZ-Homelab pre-deployment configuration wizard..."
# Check if running interactively
if ! ui_available || $non_interactive; then
print_error "This script requires an interactive terminal with dialog/whiptail"
print_error "Run without --no-ui or in a proper terminal"
exit 1
fi
# Show welcome screen
show_welcome || exit 1
# Get configuration interactively
local domain
domain=$(get_domain_config) || exit 1
local timezone
timezone=$(get_timezone) || exit 1
local puid pgid
read -r puid pgid <<< "$(get_user_ids)" || exit 1
local deployment_type
deployment_type=$(get_deployment_type) || exit 1
# Get service selection
local services
services=$(get_service_selection "$deployment_type")
# Confirm configuration
if ! confirm_configuration "$domain" "$timezone" "$puid" "$pgid" "$deployment_type" "$services"; then
print_info "Configuration cancelled by user"
exit 0
fi
# Validate configuration
if ! validate_configuration "$domain" "$deployment_type"; then
print_error "Configuration validation failed"
exit 1
fi
# Create Docker networks
if ! create_docker_networks; then
print_error "Failed to create Docker networks"
exit 1
fi
# Generate .env file
if ! generate_env_file "$domain" "$timezone" "$puid" "$pgid" "$deployment_type"; then
print_error "Failed to generate .env file"
exit 1
fi
echo ""
print_success "EZ-Homelab configuration complete!"
print_info "Next steps:"
print_info "1. Edit $EZ_HOME/.env to set your actual secrets"
print_info "2. Run: ./validate.sh"
print_info "3. Run: ./localize.sh"
print_info "4. Run: ./deploy.sh core"
exit 0
}
# Run main function
main "$@"

View File

@@ -0,0 +1,373 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Pre-Flight System Validation
# Performs comprehensive system checks before EZ-Homelab deployment
SCRIPT_NAME="preflight"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# SCRIPT CONFIGURATION
# =============================================================================
# Minimum requirements
MIN_DISK_SPACE=20 # GB
MIN_MEMORY=1024 # MB
MIN_CPU_CORES=2
# Required packages
REQUIRED_PACKAGES=("curl" "wget" "git" "jq")
# Optional packages (recommended)
OPTIONAL_PACKAGES=("htop" "ncdu" "tmux" "unzip")
# =============================================================================
# VALIDATION FUNCTIONS
# =============================================================================
# Check OS compatibility
check_os_compatibility() {
print_info "Checking OS compatibility..."
if ! validate_os; then
print_error "Unsupported OS: $OS_NAME $OS_VERSION"
print_error "Supported: Ubuntu 20.04+, Debian 11+, Raspberry Pi OS"
return 1
fi
print_success "OS: $OS_NAME $OS_VERSION ($ARCH)"
return 0
}
# Check system resources
check_system_resources() {
print_info "Checking system resources..."
local errors=0
# Check disk space
local disk_space
disk_space=$(get_disk_space)
if (( disk_space < MIN_DISK_SPACE )); then
print_error "Insufficient disk space: ${disk_space}GB available, ${MIN_DISK_SPACE}GB required"
((errors++))
else
print_success "Disk space: ${disk_space}GB available"
fi
# Check memory
local total_memory
total_memory=$(get_total_memory)
if (( total_memory < MIN_MEMORY )); then
print_error "Insufficient memory: ${total_memory}MB available, ${MIN_MEMORY}MB required"
((errors++))
else
print_success "Memory: ${total_memory}MB total"
fi
# Check CPU cores
local cpu_cores
cpu_cores=$(nproc)
if (( cpu_cores < MIN_CPU_CORES )); then
print_warning "Low CPU cores: ${cpu_cores} available, ${MIN_CPU_CORES} recommended"
else
print_success "CPU cores: $cpu_cores"
fi
return $errors
}
# Check network connectivity
check_network_connectivity() {
print_info "Checking network connectivity..."
if ! check_network; then
print_error "No internet connection detected"
print_error "Please check your network configuration"
return 1
fi
print_success "Internet connection available"
# Check DNS resolution
if ! nslookup github.com >/dev/null 2>&1; then
print_warning "DNS resolution may be slow or failing"
else
print_success "DNS resolution working"
fi
return 0
}
# Check required packages
check_required_packages() {
print_info "Checking required packages..."
local missing=()
for package in "${REQUIRED_PACKAGES[@]}"; do
if ! command_exists "$package"; then
missing+=("$package")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
print_error "Missing required packages: ${missing[*]}"
print_info "Install with: sudo apt update && sudo apt install -y ${missing[*]}"
return 1
fi
print_success "All required packages installed"
return 0
}
# Check optional packages
check_optional_packages() {
print_info "Checking optional packages..."
local missing=()
for package in "${OPTIONAL_PACKAGES[@]}"; do
if ! command_exists "$package"; then
missing+=("$package")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
print_warning "Optional packages not installed: ${missing[*]}"
print_info "Consider installing for better experience: sudo apt install -y ${missing[*]}"
else
print_success "All optional packages available"
fi
return 0
}
# Check Docker installation
check_docker_installation() {
print_info "Checking Docker installation..."
if ! command_exists docker; then
print_warning "Docker not installed"
print_info "Docker will be installed by setup.sh"
return 2 # Warning
fi
if ! service_running docker; then
print_warning "Docker service not running"
print_info "Docker will be started by setup.sh"
return 2
fi
# Check Docker version
local docker_version
docker_version=$(docker --version | grep -oP 'Docker version \K[^,]+')
if [[ -z "$docker_version" ]]; then
print_warning "Could not determine Docker version"
return 2
fi
# Compare version (simplified check)
if [[ "$docker_version" =~ ^([0-9]+)\.([0-9]+) ]]; then
local major="${BASH_REMATCH[1]}"
local minor="${BASH_REMATCH[2]}"
if (( major < 20 || (major == 20 && minor < 10) )); then
print_warning "Docker version $docker_version may be outdated (20.10+ recommended)"
return 2
fi
fi
print_success "Docker $docker_version installed and running"
return 0
}
# Check NVIDIA GPU
check_nvidia_gpu() {
print_info "Checking for NVIDIA GPU..."
if ! command_exists nvidia-smi; then
print_info "No NVIDIA GPU detected or drivers not installed"
return 0
fi
local gpu_info
gpu_info=$(nvidia-smi --query-gpu=name --format=csv,noheader,nounits | head -1)
if [[ -z "$gpu_info" ]]; then
print_warning "NVIDIA GPU detected but not accessible"
return 2
fi
print_success "NVIDIA GPU: $gpu_info"
return 0
}
# Check EZ-Homelab repository
check_repository() {
print_info "Checking EZ-Homelab repository..."
if [[ ! -d "$EZ_HOME" ]]; then
print_error "EZ-Homelab repository not found at $EZ_HOME"
print_error "Please clone the repository first"
return 1
fi
if [[ ! -f "$EZ_HOME/docker-compose/core/docker-compose.yml" ]]; then
print_error "Repository structure incomplete"
print_error "Please ensure you have the full EZ-Homelab repository"
return 1
fi
print_success "EZ-Homelab repository found at $EZ_HOME"
return 0
}
# Check user permissions
check_user_permissions() {
print_info "Checking user permissions..."
if is_root; then
print_warning "Running as root - not recommended for normal usage"
print_info "Consider running as regular user with sudo access"
return 2
fi
if ! sudo -n true 2>/dev/null; then
print_error "User does not have sudo access"
print_error "Please ensure your user can run sudo commands"
return 1
fi
print_success "User has appropriate permissions"
return 0
}
# =============================================================================
# REPORT GENERATION
# =============================================================================
# Generate validation report
generate_report() {
local report_file="$LOG_DIR/preflight-report-$(date +%Y%m%d-%H%M%S).txt"
{
echo "EZ-Homelab Pre-Flight Validation Report"
echo "======================================="
echo "Date: $(date)"
echo "System: $OS_NAME $OS_VERSION ($ARCH)"
echo "Kernel: $KERNEL_VERSION"
echo "User: $EZ_USER (UID: $EZ_UID, GID: $EZ_GID)"
echo ""
echo "Results:"
echo "- OS Compatibility: $(check_os_compatibility >/dev/null 2>&1 && echo "PASS" || echo "FAIL")"
echo "- System Resources: $(check_system_resources >/dev/null 2>&1 && echo "PASS" || echo "WARN/FAIL")"
echo "- Network: $(check_network_connectivity >/dev/null 2>&1 && echo "PASS" || echo "FAIL")"
echo "- Required Packages: $(check_required_packages >/dev/null 2>&1 && echo "PASS" || echo "FAIL")"
echo "- Docker: $(check_docker_installation >/dev/null 2>&1; case $? in 0) echo "PASS";; 1) echo "FAIL";; 2) echo "WARN";; esac)"
echo "- NVIDIA GPU: $(check_nvidia_gpu >/dev/null 2>&1 && echo "PASS" || echo "N/A")"
echo "- Repository: $(check_repository >/dev/null 2>&1 && echo "PASS" || echo "FAIL")"
echo "- Permissions: $(check_user_permissions >/dev/null 2>&1; case $? in 0) echo "PASS";; 1) echo "FAIL";; 2) echo "WARN";; esac)"
echo ""
echo "Log file: $LOG_FILE"
} > "$report_file"
print_info "Report saved to: $report_file"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local non_interactive=false
local verbose=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
ui_show_help "$SCRIPT_NAME"
exit 0
;;
--no-ui)
non_interactive=true
;;
-v|--verbose)
verbose=true
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
shift
done
# Initialize script
init_script "$SCRIPT_NAME"
if $verbose; then
set -x
fi
print_info "Starting EZ-Homelab pre-flight validation..."
print_info "This will check your system readiness for EZ-Homelab deployment."
local total_checks=9
local passed=0
local warnings=0
local failed=0
# Run all checks
local checks=(
"check_os_compatibility"
"check_system_resources"
"check_network_connectivity"
"check_required_packages"
"check_optional_packages"
"check_docker_installation"
"check_nvidia_gpu"
"check_repository"
"check_user_permissions"
)
for check in "${checks[@]}"; do
echo ""
# Run check and capture exit code
local exit_code=0
$check || exit_code=$?
if [[ $exit_code -eq 0 ]]; then
((passed++))
elif [[ $exit_code -eq 2 ]]; then
((warnings++))
else
((failed++))
fi
done
echo ""
print_info "Pre-flight validation complete!"
print_info "Summary: $passed passed, $warnings warnings, $failed failed"
# Generate report
generate_report
# Determine exit code
if [[ $failed -gt 0 ]]; then
print_error "Critical issues found. Please resolve before proceeding."
print_info "Check the log file: $LOG_FILE"
print_info "Run this script again after fixing issues."
exit 1
elif [[ $warnings -gt 0 ]]; then
print_warning "Some warnings detected. You may proceed but consider addressing them."
exit 2
else
print_success "All checks passed! Your system is ready for EZ-Homelab deployment."
exit 0
fi
}
# Run main function
main "$@"

556
scripts/enhanced-setup/service.sh Executable file
View File

@@ -0,0 +1,556 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Service Management
# Individual service control, monitoring, and maintenance
SCRIPT_NAME="service"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# SERVICE MANAGEMENT CONFIGURATION
# =============================================================================
# Service action timeouts (seconds)
SERVICE_START_TIMEOUT=60
SERVICE_STOP_TIMEOUT=30
LOG_TAIL_LINES=100
HEALTH_CHECK_RETRIES=3
# =============================================================================
# SERVICE DISCOVERY FUNCTIONS
# =============================================================================
# Find all available services across all stacks
find_all_services() {
local services=()
# Get all docker-compose directories
local compose_dirs
mapfile -t compose_dirs < <(find "$EZ_HOME/docker-compose" -name "docker-compose.yml" -type f -exec dirname {} \; 2>/dev/null)
for dir in "${compose_dirs[@]}"; do
local stack_services
mapfile -t stack_services < <(get_stack_services "$(basename "$dir")")
for service in "${stack_services[@]}"; do
# Avoid duplicates
if [[ ! " ${services[*]} " =~ " ${service} " ]]; then
services+=("$service")
fi
done
done
printf '%s\n' "${services[@]}" | sort
}
# Find which stack a service belongs to
find_service_stack() {
local service="$1"
local compose_dirs
mapfile -t compose_dirs < <(find "$EZ_HOME/docker-compose" -name "docker-compose.yml" -type f -exec dirname {} \; 2>/dev/null)
for dir in "${compose_dirs[@]}"; do
local stack_services
mapfile -t stack_services < <(get_stack_services "$(basename "$dir")")
for stack_service in "${stack_services[@]}"; do
if [[ "$stack_service" == "$service" ]]; then
echo "$dir"
return 0
fi
done
done
return 1
}
# Get service compose file
get_service_compose_file() {
local service="$1"
local stack_dir
stack_dir=$(find_service_stack "$service")
[[ -n "$stack_dir" ]] && echo "$stack_dir/docker-compose.yml"
}
# =============================================================================
# SERVICE CONTROL FUNCTIONS
# =============================================================================
# Start a specific service
start_service() {
local service="$1"
local compose_file
compose_file=$(get_service_compose_file "$service")
if [[ -z "$compose_file" ]]; then
print_error "Service '$service' not found"
return 1
fi
if is_service_running "$service"; then
print_warning "Service '$service' is already running"
return 0
fi
print_info "Starting service: $service"
local compose_dir=$(dirname "$compose_file")
local compose_base=$(basename "$compose_file")
if (cd "$compose_dir" && docker compose -f "$compose_base" up -d "$service"); then
print_info "Waiting for service to start..."
sleep "$SERVICE_START_TIMEOUT"
if is_service_running "$service"; then
print_success "Service '$service' started successfully"
return 0
else
print_error "Service '$service' failed to start"
return 1
fi
else
print_error "Failed to start service '$service'"
return 1
fi
}
# Stop a specific service
stop_service() {
local service="$1"
local compose_file
compose_file=$(get_service_compose_file "$service")
if [[ -z "$compose_file" ]]; then
print_error "Service '$service' not found"
return 1
fi
if ! is_service_running "$service"; then
print_warning "Service '$service' is not running"
return 0
fi
print_info "Stopping service: $service"
local compose_dir=$(dirname "$compose_file")
local compose_base=$(basename "$compose_file")
if (cd "$compose_dir" && docker compose -f "$compose_base" stop "$service"); then
local count=0
while ((count < SERVICE_STOP_TIMEOUT)) && is_service_running "$service"; do
sleep 1
((count++))
done
if ! is_service_running "$service"; then
print_success "Service '$service' stopped successfully"
return 0
else
print_warning "Service '$service' did not stop gracefully, forcing..."
(cd "$compose_dir" && docker compose -f "$compose_base" kill "$service")
return 0
fi
else
print_error "Failed to stop service '$service'"
return 1
fi
}
# Restart a specific service
restart_service() {
local service="$1"
print_info "Restarting service: $service"
if stop_service "$service" && start_service "$service"; then
print_success "Service '$service' restarted successfully"
return 0
else
print_error "Failed to restart service '$service'"
return 1
fi
}
# Get service logs
show_service_logs() {
local service="$1"
local lines="${2:-$LOG_TAIL_LINES}"
local follow="${3:-false}"
local compose_file
compose_file=$(get_service_compose_file "$service")
if [[ -z "$compose_file" ]]; then
print_error "Service '$service' not found"
return 1
fi
print_info "Showing logs for service: $service"
local compose_dir=$(dirname "$compose_file")
local compose_base=$(basename "$compose_file")
if $follow; then
(cd "$compose_dir" && docker compose -f "$compose_base" logs -f --tail="$lines" "$service")
else
(cd "$compose_dir" && docker compose -f "$compose_base" logs --tail="$lines" "$service")
fi
}
# Check service health
check_service_status() {
local service="$1"
local compose_file
compose_file=$(get_service_compose_file "$service")
if [[ -z "$compose_file" ]]; then
print_error "Service '$service' not found"
return 1
fi
echo "Service: $service"
if is_service_running "$service"; then
echo "Status: ✅ Running"
# Get container info
local container_info
container_info=$(docker ps --filter "name=^${service}$" --format "table {{.Image}}\t{{.Status}}\t{{.Ports}}" | tail -n +2)
if [[ -n "$container_info" ]]; then
echo "Container: $container_info"
fi
# Get health status if available
local health_status
health_status=$(docker inspect "$service" --format '{{.State.Health.Status}}' 2>/dev/null || echo "N/A")
if [[ "$health_status" != "N/A" ]]; then
echo "Health: $health_status"
fi
else
echo "Status: ❌ Stopped"
fi
# Show stack info
local stack_dir
stack_dir=$(find_service_stack "$service")
if [[ -n "$stack_dir" ]]; then
echo "Stack: $(basename "$stack_dir")"
fi
echo
}
# Execute command in service container
exec_service_command() {
local service="$1"
shift
local command="$*"
if ! is_service_running "$service"; then
print_error "Service '$service' is not running"
return 1
fi
print_info "Executing command in $service: $command"
docker exec -it "$service" $command
}
# =============================================================================
# BULK OPERATIONS
# =============================================================================
# Start all services in a stack
start_stack_services() {
local stack="$1"
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then
print_error "Stack '$stack' not found"
return 1
fi
print_info "Starting all services in stack: $stack"
if docker compose -f "$compose_file" up -d; then
print_success "Stack '$stack' started successfully"
return 0
else
print_error "Failed to start stack '$stack'"
return 1
fi
}
# Stop all services in a stack
stop_stack_services() {
local stack="$1"
local compose_file="$EZ_HOME/docker-compose/$stack/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then
print_error "Stack '$stack' not found"
return 1
fi
print_info "Stopping all services in stack: $stack"
if docker compose -f "$compose_file" down; then
print_success "Stack '$stack' stopped successfully"
return 0
else
print_error "Failed to stop stack '$stack'"
return 1
fi
}
# Show status of all services
show_all_status() {
print_info "EZ-Homelab Service Status"
echo
local services
mapfile -t services < <(find_all_services)
for service in "${services[@]}"; do
check_service_status "$service"
done
}
# =============================================================================
# UTILITY FUNCTIONS
# =============================================================================
# List all available services
list_services() {
print_info "Available Services:"
local services
mapfile -t services < <(find_all_services)
for service in "${services[@]}"; do
local stack_dir=""
stack_dir=$(find_service_stack "$service")
local stack_name=""
[[ -n "$stack_dir" ]] && stack_name="($(basename "$stack_dir"))"
local status="❌ Stopped"
is_service_running "$service" && status="✅ Running"
printf " %-20s %-12s %s\n" "$service" "$status" "$stack_name"
done
}
# Clean up stopped containers and unused images
cleanup_services() {
print_info "Cleaning up Docker resources..."
# Remove stopped containers
local stopped_containers
stopped_containers=$(docker ps -aq -f status=exited)
if [[ -n "$stopped_containers" ]]; then
print_info "Removing stopped containers..."
echo "$stopped_containers" | xargs docker rm
fi
# Remove unused images
print_info "Removing unused images..."
docker image prune -f
# Remove unused volumes
print_info "Removing unused volumes..."
docker volume prune -f
print_success "Cleanup completed"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local action=""
local service=""
local stack=""
local follow_logs=false
local log_lines="$LOG_TAIL_LINES"
local non_interactive=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << EOF
EZ-Homelab Service Management
USAGE:
service [OPTIONS] <ACTION> [SERVICE|STACK]
ACTIONS:
start Start a service or all services in a stack
stop Stop a service or all services in a stack
restart Restart a service or all services in a stack
status Show status of a service or all services
logs Show logs for a service
exec Execute command in a running service container
list List all available services
cleanup Clean up stopped containers and unused resources
ARGUMENTS:
SERVICE Service name (for service-specific actions)
STACK Stack name (for stack-wide actions)
OPTIONS:
-f, --follow Follow logs (for logs action)
-n, --lines NUM Number of log lines to show (default: $LOG_TAIL_LINES)
--no-ui Run without interactive UI
EXAMPLES:
service list # List all services
service status # Show all service statuses
service start traefik # Start Traefik service
service stop core # Stop all core services
service restart pihole # Restart Pi-hole service
service logs traefik # Show Traefik logs
service logs traefik --follow # Follow Traefik logs
service exec authelia bash # Execute bash in Authelia container
service cleanup # Clean up Docker resources
EOF
exit 0
;;
-f|--follow)
follow_logs=true
shift
;;
-n|--lines)
log_lines="$2"
shift 2
;;
--no-ui)
non_interactive=true
shift
;;
start|stop|restart|status|logs|exec|list|cleanup)
if [[ -z "$action" ]]; then
action="$1"
else
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Too many arguments"
exit 1
fi
fi
shift
;;
*)
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Too many arguments"
exit 1
fi
shift
;;
esac
done
# Initialize script
init_script "$SCRIPT_NAME" "$SCRIPT_VERSION"
init_logging "$SCRIPT_NAME"
# Check prerequisites
if ! docker_available; then
print_error "Docker is not available"
exit 1
fi
# Execute action
case "$action" in
start)
if [[ -n "$service" ]]; then
# Check if it's a stack or service
if [[ -d "$EZ_HOME/docker-compose/$service" ]]; then
start_stack_services "$service"
else
start_service "$service"
fi
else
print_error "Service or stack name required"
exit 1
fi
;;
stop)
if [[ -n "$service" ]]; then
# Check if it's a stack or service
if [[ -d "$EZ_HOME/docker-compose/$service" ]]; then
stop_stack_services "$service"
else
stop_service "$service"
fi
else
print_error "Service or stack name required"
exit 1
fi
;;
restart)
if [[ -n "$service" ]]; then
# Check if it's a stack or service
if [[ -d "$EZ_HOME/docker-compose/$service" ]]; then
stop_stack_services "$service" && start_stack_services "$service"
else
restart_service "$service"
fi
else
print_error "Service or stack name required"
exit 1
fi
;;
status)
if [[ -n "$service" ]]; then
check_service_status "$service"
else
show_all_status
fi
;;
logs)
if [[ -n "$service" ]]; then
show_service_logs "$service" "$log_lines" "$follow_logs"
else
print_error "Service name required"
exit 1
fi
;;
exec)
if [[ -n "$service" ]]; then
if [[ $# -gt 0 ]]; then
exec_service_command "$service" "$@"
else
exec_service_command "$service" bash
fi
else
print_error "Service name required"
exit 1
fi
;;
list)
list_services
;;
cleanup)
cleanup_services
;;
"")
print_error "No action specified. Use --help for usage information."
exit 1
;;
*)
print_error "Unknown action: $action"
exit 1
;;
esac
}
# Run main function
main "$@"

383
scripts/enhanced-setup/setup.sh Executable file
View File

@@ -0,0 +1,383 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - System Setup and Prerequisites
# Installs Docker and configures system prerequisites for EZ-Homelab
SCRIPT_NAME="setup"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# SCRIPT CONFIGURATION
# =============================================================================
# Docker version requirements
MIN_DOCKER_VERSION="20.10.0"
RECOMMENDED_DOCKER_VERSION="24.0.0"
# Required system packages
SYSTEM_PACKAGES=("curl" "wget" "git" "jq" "unzip" "software-properties-common" "apt-transport-https" "ca-certificates" "gnupg" "lsb-release")
# Python packages (for virtual environment)
PYTHON_PACKAGES=("docker-compose" "pyyaml" "requests")
# =============================================================================
# DOCKER INSTALLATION FUNCTIONS
# =============================================================================
# Remove old Docker installations
remove_old_docker() {
print_info "Removing old Docker installations..."
# Stop services
sudo systemctl stop docker docker.socket containerd 2>/dev/null || true
# Remove packages
sudo apt remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true
# Remove Docker data
sudo rm -rf /var/lib/docker /var/lib/containerd
print_success "Old Docker installations removed"
}
# Install Docker using official method
install_docker_official() {
print_info "Installing Docker Engine (official method)..."
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/"$(lsb_release -si | tr '[:upper:]' '[:lower:]')"/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Add Docker repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/$(lsb_release -si | tr '[:upper:]' '[:lower:]') $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Update package index
sudo apt update
# Install Docker Engine
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
print_success "Docker Engine installed"
}
# Install Docker using convenience script (fallback)
install_docker_convenience() {
print_info "Installing Docker using convenience script..."
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
rm get-docker.sh
print_success "Docker installed via convenience script"
}
# Configure Docker daemon
configure_docker_daemon() {
print_info "Configuring Docker daemon..."
local daemon_config='{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"iptables": false,
"bridge": "none",
"ip-masq": false
}'
echo "$daemon_config" | sudo tee /etc/docker/daemon.json > /dev/null
print_success "Docker daemon configured"
}
# Start and enable Docker service
start_docker_service() {
print_info "Starting Docker service..."
sudo systemctl enable docker
sudo systemctl start docker
# Wait for Docker to be ready
local retries=30
while ! docker info >/dev/null 2>&1 && (( retries > 0 )); do
sleep 1
((retries--))
done
if ! docker info >/dev/null 2>&1; then
print_error "Docker service failed to start"
return 1
fi
print_success "Docker service started and enabled"
}
# Add user to docker group
configure_user_permissions() {
print_info "Configuring user permissions..."
if ! groups "$EZ_USER" | grep -q docker; then
sudo usermod -aG docker "$EZ_USER"
print_warning "User added to docker group. A reboot may be required for changes to take effect."
print_info "Alternatively, run: newgrp docker"
else
print_success "User already in docker group"
fi
}
# Test Docker installation
test_docker_installation() {
print_info "Testing Docker installation..."
# Run hello-world container
if ! docker run --rm hello-world >/dev/null 2>&1; then
print_error "Docker test failed"
return 1
fi
# Check Docker version
local docker_version
docker_version=$(docker --version | grep -oP 'Docker version \K[^,]+')
if [[ -z "$docker_version" ]]; then
print_warning "Could not determine Docker version"
return 2
fi
print_success "Docker $docker_version installed and working"
# Check Docker Compose V2
if docker compose version >/dev/null 2>&1; then
local compose_version
compose_version=$(docker compose version | grep -oP 'v\K[^ ]+')
print_success "Docker Compose V2 $compose_version available"
else
print_warning "Docker Compose V2 not available"
fi
}
# =============================================================================
# SYSTEM SETUP FUNCTIONS
# =============================================================================
# Install system packages
install_system_packages() {
print_info "Installing system packages..."
sudo apt update
local missing_packages=()
for package in "${SYSTEM_PACKAGES[@]}"; do
if ! dpkg -l "$package" >/dev/null 2>&1; then
missing_packages+=("$package")
fi
done
if [[ ${#missing_packages[@]} -gt 0 ]]; then
sudo apt install -y "${missing_packages[@]}"
fi
print_success "System packages installed"
}
# Set up Python virtual environment
setup_python_environment() {
print_info "Setting up Python virtual environment..."
local venv_dir="$HOME/.ez-homelab-venv"
# Create virtual environment
if [[ ! -d "$venv_dir" ]]; then
python3 -m venv "$venv_dir"
fi
# Activate and install packages
source "$venv_dir/bin/activate"
# Upgrade pip
pip install --upgrade pip
# Install required packages
if $IS_ARM64; then
# Use PiWheels for ARM64
pip install --extra-index-url https://www.piwheels.org/simple "${PYTHON_PACKAGES[@]}"
else
pip install "${PYTHON_PACKAGES[@]}"
fi
# Deactivate
deactivate
print_success "Python virtual environment configured"
}
# Configure system settings
configure_system_settings() {
print_info "Configuring system settings..."
# Increase file watchers (for large deployments)
echo "fs.inotify.max_user_watches=524288" | sudo tee -a /etc/sysctl.conf >/dev/null
sudo sysctl -p >/dev/null 2>&1
# Configure journald for better logging
sudo mkdir -p /etc/systemd/journald.conf.d
cat << EOF | sudo tee /etc/systemd/journald.conf.d/ez-homelab.conf >/dev/null
[Journal]
Storage=persistent
SystemMaxUse=100M
RuntimeMaxUse=50M
EOF
print_success "System settings configured"
}
# Create required directories
create_directories() {
print_info "Creating required directories..."
sudo mkdir -p /opt/stacks
sudo chown "$EZ_USER:$EZ_USER" /opt/stacks
mkdir -p "$LOG_DIR"
print_success "Directories created"
}
# =============================================================================
# NVIDIA GPU SETUP (OPTIONAL)
# =============================================================================
# Check if NVIDIA setup is needed
check_nvidia_setup_needed() {
command_exists nvidia-smi && nvidia-smi >/dev/null 2>&1
}
# Install NVIDIA drivers (if requested)
install_nvidia_drivers() {
if ! ui_yesno "NVIDIA GPU detected. Install NVIDIA drivers and Docker GPU support?"; then
print_info "Skipping NVIDIA setup"
return 0
fi
print_info "Installing NVIDIA drivers..."
# Add NVIDIA repository
wget https://developer.download.nvidia.com/compute/cuda/repos/"$(lsb_release -si | tr '[:upper:]' '[:lower:]')""$(lsb_release -sr | tr -d '.')"/x86_64/cuda-keyring_1.0-1_all.deb
sudo dpkg -i cuda-keyring_1.0-1_all.deb
rm cuda-keyring_1.0-1_all.deb
sudo apt update
# Install NVIDIA driver
sudo apt install -y nvidia-driver-525 nvidia-docker2
# Configure Docker for NVIDIA
sudo systemctl restart docker
print_success "NVIDIA drivers installed"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local skip_docker=false
local skip_nvidia=false
local non_interactive=false
local verbose=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
ui_show_help "$SCRIPT_NAME"
exit 0
;;
--skip-docker)
skip_docker=true
;;
--skip-nvidia)
skip_nvidia=true
;;
--no-ui)
non_interactive=true
;;
-v|--verbose)
verbose=true
;;
*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
shift
done
# Initialize script
init_script "$SCRIPT_NAME"
if $verbose; then
set -x
fi
print_info "Starting EZ-Homelab system setup..."
print_info "This will install Docker and configure your system for EZ-Homelab."
# Run pre-flight checks first
if ! "$(dirname "${BASH_SOURCE[0]}")/preflight.sh" --no-ui; then
print_error "Pre-flight checks failed. Please resolve issues before proceeding."
exit 1
fi
# Install system packages
run_with_progress "Installing system packages" "install_system_packages"
# Set up Python environment
run_with_progress "Setting up Python environment" "setup_python_environment"
# Configure system settings
run_with_progress "Configuring system settings" "configure_system_settings"
# Create directories
run_with_progress "Creating directories" "create_directories"
# Install Docker (unless skipped)
if ! $skip_docker; then
run_with_progress "Removing old Docker installations" "remove_old_docker"
run_with_progress "Installing Docker" "install_docker_official"
run_with_progress "Configuring Docker daemon" "configure_docker_daemon"
run_with_progress "Starting Docker service" "start_docker_service"
run_with_progress "Configuring user permissions" "configure_user_permissions"
run_with_progress "Testing Docker installation" "test_docker_installation"
else
print_info "Skipping Docker installation (--skip-docker)"
fi
# NVIDIA setup (if applicable and not skipped)
if ! $skip_nvidia && check_nvidia_setup_needed; then
run_with_progress "Installing NVIDIA drivers" "install_nvidia_drivers"
fi
echo ""
print_success "EZ-Homelab system setup complete!"
if ! $skip_docker && ! groups "$EZ_USER" | grep -q docker; then
print_warning "IMPORTANT: Please reboot your system for Docker group changes to take effect."
print_info "Alternatively, run: newgrp docker"
print_info "Then re-run this script or proceed to the next step."
else
print_info "You can now proceed to the pre-deployment wizard:"
print_info " ./pre-deployment-wizard.sh"
fi
exit 0
}
# Run main function
main "$@"

View File

@@ -0,0 +1,150 @@
# EZ-Homelab Enhanced Setup Scripts - Standards & Conventions
## Script Communication & Standards
### Exit Codes
- **0**: Success - Script completed without issues
- **1**: Error - Script failed, requires user intervention
- **2**: Warning - Script completed but with non-critical issues
- **3**: Skipped - Script skipped due to conditions (e.g., already installed)
### Logging
- **Location**: `/var/log/ez-homelab/` (created by setup.sh)
- **Format**: `YYYY-MM-DD HH:MM:SS [SCRIPT_NAME] LEVEL: MESSAGE`
- **Levels**: INFO, WARN, ERROR, DEBUG
- **Rotation**: Use logrotate with weekly rotation, keep 4 weeks
### Shared Variables (lib/common.sh)
```bash
# Repository and paths
EZ_HOME="${EZ_HOME:-/home/kelin/EZ-Homelab}"
STACKS_DIR="${STACKS_DIR:-/opt/stacks}"
LOG_DIR="${LOG_DIR:-/var/log/ez-homelab}"
# User and system
EZ_USER="${EZ_USER:-$USER}"
EZ_UID="${EZ_UID:-$(id -u)}"
EZ_GID="${EZ_GID:-$(id -g)}"
# Architecture detection
ARCH="$(uname -m)"
IS_ARM64=false
[[ "$ARCH" == "aarch64" ]] && IS_ARM64=true
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
```
### Configuration Files
- **Format**: Use YAML for complex configurations, .env for environment variables
- **Location**: `scripts/enhanced-setup/config/` for script configs
- **Validation**: All configs validated with `yq` (YAML) or `dotenv` (env)
### Function Naming
- **Prefix**: Use script name (e.g., `preflight_check_disk()`)
- **Style**: snake_case for functions, UPPER_CASE for constants
- **Documentation**: All functions have header comments with purpose, parameters, return values
## UI/UX Design
### Dialog/Whiptail Theme
- **Colors**: Blue headers (#0000FF), Green success (#00FF00), Red errors (#FF0000)
- **Size**: Auto-size based on content, minimum 80x24
- **Title**: "EZ-Homelab Setup - [Script Name]"
- **Backtitle**: "EZ-Homelab Enhanced Setup Scripts v1.0"
### Menu Flow
- **Navigation**: Tab/Arrow keys, Enter to select, Esc to cancel
- **Progress**: Use `--gauge` for long operations with percentage
- **Confirmation**: Always confirm destructive actions with "Are you sure? (y/N)"
- **Help**: F1 key shows context help, `--help` flag for command-line usage
### User Prompts
- **Style**: Clear, action-oriented (e.g., "Press Enter to continue" not "OK")
- **Defaults**: Safe defaults (e.g., N for destructive actions)
- **Validation**: Real-time input validation with error messages
## Error Handling & Recovery
### Error Types
- **Critical**: Script cannot continue (exit 1)
- **Warning**: Issue noted but script continues (exit 2)
- **Recoverable**: User can fix and retry
### Recovery Mechanisms
- **Backups**: Automatic backup of modified files (`.bak` extension)
- **Rollback**: `--rollback` flag to undo last operation
- **Resume**: Scripts detect partial completion and offer to resume
- **Cleanup**: `--cleanup` flag removes temporary files and partial installs
### User Guidance
- **Error Messages**: Include suggested fix (e.g., "Run 'sudo apt update' and retry")
- **Logs**: Point to log file location for detailed errors
- **Support**: Include link to documentation or issue tracker
## Testing & Validation
### Unit Testing
- **Tool**: ShellCheck for syntax validation
- **Coverage**: All scripts pass ShellCheck with no warnings
- **Mocks**: Use `mktemp` and environment variables to mock external calls
### Integration Testing
- **Environments**:
- AMD64: Ubuntu 22.04 LTS VM
- ARM64: Raspberry Pi OS (64-bit) on Pi 4
- **Scenarios**: Clean install, partial install recovery, network failures
- **Automation**: Use GitHub Actions for CI/CD with matrix testing
### Validation Checks
- **Pre-run**: Scripts validate dependencies and environment
- **Post-run**: Verify expected files, services, and configurations
- **Cross-script**: Ensure scripts don't conflict (e.g., multiple network creations)
## Integration Points
### Existing EZ-Homelab Structure
- **Repository**: Scripts read from `$EZ_HOME/docker-compose/` and `$EZ_HOME/.env`
- **Runtime**: Deploy to `$STACKS_DIR/` matching current structure
- **Services**: Leverage existing compose files without modification
- **Secrets**: Use existing `.env` pattern, never commit secrets
### Service Dependencies
- **Core First**: All scripts enforce core stack deployment before others
- **Network Requirements**: Scripts create `traefik-network` and `homelab-network` as needed
- **Port Conflicts**: Validate no conflicts before deployment
- **Health Checks**: Use Docker health checks where available
### Version Compatibility
- **Docker**: Support 20.10+ with Compose V2
- **OS**: Debian 11+, Ubuntu 20.04+, Raspbian/Raspberry Pi OS
- **Architecture**: AMD64 and ARM64 with PiWheels for Python packages
## Development Workflow
### Branching Strategy
- **Main**: Production-ready code
- **Develop**: Integration branch
- **Feature**: `feature/script-name` for individual scripts
- **Hotfix**: `hotfix/issue-description` for urgent fixes
### Code Reviews
- **Required**: All PRs need review from at least one maintainer
- **Checklist**: Standards compliance, testing, documentation
- **Automation**: GitHub Actions for basic checks (ShellCheck, YAML validation)
### Documentation
- **Inline**: All functions and complex logic documented
- **README**: Each script has usage examples
- **Updates**: PRD updated with implemented features
- **Changelog**: Maintain `CHANGELOG.md` with version history
### Release Process
- **Versioning**: Semantic versioning (MAJOR.MINOR.PATCH)
- **Testing**: Full integration test before release
- **Packaging**: Scripts distributed as part of EZ-Homelab repository
- **Announcement**: Release notes with breaking changes highlighted

600
scripts/enhanced-setup/update.sh Executable file
View File

@@ -0,0 +1,600 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Update Management
# Service update management with zero-downtime deployments
SCRIPT_NAME="update"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# UPDATE CONFIGURATION
# =============================================================================
# Update settings
UPDATE_CHECK_INTERVAL=86400 # 24 hours in seconds
UPDATE_TIMEOUT=300 # 5 minutes timeout for updates
ROLLBACK_TIMEOUT=180 # 3 minutes for rollback
# Update sources
DOCKER_HUB_API="https://registry.hub.docker.com/v2"
GITHUB_API="https://api.github.com"
# Update strategies
UPDATE_STRATEGY_ROLLING="rolling" # Update one service at a time
UPDATE_STRATEGY_BLUE_GREEN="blue-green" # Deploy new version alongside old
UPDATE_STRATEGY_CANARY="canary" # Update subset of instances first
# Default update strategy
DEFAULT_UPDATE_STRATEGY="$UPDATE_STRATEGY_ROLLING"
# =============================================================================
# UPDATE STATE MANAGEMENT
# =============================================================================
# Update state file
UPDATE_STATE_FILE="$LOG_DIR/update_state.json"
# Initialize update state
init_update_state() {
if [[ ! -f "$UPDATE_STATE_FILE" ]]; then
cat > "$UPDATE_STATE_FILE" << EOF
{
"last_check": 0,
"updates_available": {},
"update_history": [],
"current_updates": {}
}
EOF
fi
}
# Record update attempt
record_update_attempt() {
local service="$1"
local old_version="$2"
local new_version="$3"
local status="$4"
local timestamp
timestamp=$(date +%s)
if command_exists "jq"; then
jq --arg service "$service" --arg old_version "$old_version" --arg new_version "$new_version" \
--arg status "$status" --argjson timestamp "$timestamp" \
'.update_history |= . + [{"service": $service, "old_version": $old_version, "new_version": $new_version, "status": $status, "timestamp": $timestamp}]' \
"$UPDATE_STATE_FILE" > "${UPDATE_STATE_FILE}.tmp" && mv "${UPDATE_STATE_FILE}.tmp" "$UPDATE_STATE_FILE"
fi
}
# Get service update status
get_service_update_status() {
local service="$1"
if command_exists "jq" && [[ -f "$UPDATE_STATE_FILE" ]]; then
jq -r ".current_updates[\"$service\"] // \"idle\"" "$UPDATE_STATE_FILE"
else
echo "unknown"
fi
}
# Set service update status
set_service_update_status() {
local service="$1"
local status="$2"
if command_exists "jq"; then
jq --arg service "$service" --arg status "$status" \
'.current_updates[$service] = $status' \
"$UPDATE_STATE_FILE" > "${UPDATE_STATE_FILE}.tmp" && mv "${UPDATE_STATE_FILE}.tmp" "$UPDATE_STATE_FILE"
fi
}
# =============================================================================
# VERSION CHECKING FUNCTIONS
# =============================================================================
# Get current service version
get_current_version() {
local service="$1"
if ! is_service_running "$service"; then
echo "unknown"
return 1
fi
# Get image from running container
local image
image=$(docker inspect "$service" --format '{{.Config.Image}}' 2>/dev/null || echo "")
if [[ -z "$image" ]]; then
echo "unknown"
return 1
fi
# Extract version tag
if [[ "$image" == *":"* ]]; then
echo "$image" | cut -d: -f2
else
echo "latest"
fi
}
# Check for Docker image updates
check_docker_updates() {
local service="$1"
if ! is_service_running "$service"; then
return 1
fi
local current_image
current_image=$(docker inspect "$service" --format '{{.Config.Image}}' 2>/dev/null || echo "")
if [[ -z "$current_image" ]]; then
return 1
fi
# Extract repository and tag
local repo tag
if [[ "$current_image" == *":"* ]]; then
repo=$(echo "$current_image" | cut -d: -f1)
tag=$(echo "$current_image" | cut -d: -f2)
else
repo="$current_image"
tag="latest"
fi
print_info "Checking updates for $service ($repo:$tag)"
# Pull latest image to check for updates
if docker pull "$repo:latest" >/dev/null 2>&1; then
# Compare image IDs
local current_id latest_id
current_id=$(docker inspect "$repo:$tag" --format '{{.Id}}' 2>/dev/null || echo "")
latest_id=$(docker inspect "$repo:latest" --format '{{.Id}}' 2>/dev/null || echo "")
if [[ "$current_id" != "$latest_id" ]]; then
print_info "Update available for $service: $tag -> latest"
return 0
else
print_info "Service $service is up to date"
return 1
fi
else
print_warning "Failed to check updates for $service"
return 1
fi
}
# Check all services for updates
check_all_updates() {
print_info "Checking for service updates"
local services
mapfile -t services < <(find_all_services)
local updates_available=()
for service in "${services[@]}"; do
if check_docker_updates "$service"; then
updates_available+=("$service")
fi
done
if [[ ${#updates_available[@]} -gt 0 ]]; then
print_info "Updates available for: ${updates_available[*]}"
return 0
else
print_info "All services are up to date"
return 1
fi
}
# =============================================================================
# UPDATE EXECUTION FUNCTIONS
# =============================================================================
# Update single service with rolling strategy
update_service_rolling() {
local service="$1"
local new_image="$2"
print_info "Updating service $service with rolling strategy"
set_service_update_status "$service" "updating"
local old_version
old_version=$(get_current_version "$service")
# Get compose file
local compose_file
compose_file=$(get_service_compose_file "$service")
if [[ -z "$compose_file" ]]; then
print_error "Cannot find compose file for service $service"
set_service_update_status "$service" "failed"
return 1
fi
local compose_dir=$(dirname "$compose_file")
local compose_base=$(basename "$compose_file")
# Backup current configuration
print_info "Creating backup before update"
"$SCRIPT_DIR/backup.sh" config --quiet
# Update the service
print_info "Pulling new image: $new_image"
if ! docker pull "$new_image"; then
print_error "Failed to pull new image: $new_image"
set_service_update_status "$service" "failed"
return 1
fi
print_info "Restarting service with new image"
if (cd "$compose_dir" && docker compose -f "$compose_base" up -d "$service"); then
# Wait for service to start
local count=0
while (( count < UPDATE_TIMEOUT )) && ! is_service_running "$service"; do
sleep 5
((count += 5))
done
if is_service_running "$service"; then
# Verify service health
sleep 10
if check_service_health "$service"; then
local new_version
new_version=$(get_current_version "$service")
print_success "Service $service updated successfully: $old_version -> $new_version"
record_update_attempt "$service" "$old_version" "$new_version" "success"
set_service_update_status "$service" "completed"
return 0
else
print_error "Service $service failed health check after update"
rollback_service "$service"
return 1
fi
else
print_error "Service $service failed to start after update"
rollback_service "$service"
return 1
fi
else
print_error "Failed to update service $service"
set_service_update_status "$service" "failed"
return 1
fi
}
# Rollback service to previous version
rollback_service() {
local service="$1"
print_warning "Rolling back service $service"
set_service_update_status "$service" "rolling_back"
# For now, just restart with current configuration
# In a more advanced implementation, this would restore from backup
local compose_file
compose_file=$(get_service_compose_file "$service")
if [[ -n "$compose_file" ]]; then
local compose_dir=$(dirname "$compose_file")
local compose_base=$(basename "$compose_file")
if (cd "$compose_dir" && docker compose -f "$compose_base" restart "$service"); then
sleep 10
if check_service_health "$service"; then
print_success "Service $service rolled back successfully"
set_service_update_status "$service" "rolled_back"
return 0
fi
fi
fi
print_error "Failed to rollback service $service"
set_service_update_status "$service" "rollback_failed"
return 1
}
# Update all services
update_all_services() {
local strategy="${1:-$DEFAULT_UPDATE_STRATEGY}"
print_info "Updating all services with $strategy strategy"
local services
mapfile -t services < <(find_all_services)
local updated=0
local failed=0
for service in "${services[@]}"; do
if check_docker_updates "$service"; then
print_info "Updating service: $service"
# Get latest image
local current_image
current_image=$(docker inspect "$service" --format '{{.Config.Image}}' 2>/dev/null || echo "")
if [[ -n "$current_image" ]]; then
local repo
repo=$(echo "$current_image" | cut -d: -f1)
local new_image="$repo:latest"
if update_service_rolling "$service" "$new_image"; then
((updated++))
else
((failed++))
fi
fi
fi
done
print_info "Update summary: $updated updated, $failed failed"
if (( failed > 0 )); then
return 1
else
return 0
fi
}
# =============================================================================
# UPDATE MONITORING FUNCTIONS
# =============================================================================
# Show update status
show_update_status() {
print_info "Update Status"
echo
local services
mapfile -t services < <(find_all_services)
echo "Service Update Status:"
echo "----------------------------------------"
for service in "${services[@]}"; do
local status
status=$(get_service_update_status "$service")
local version
version=$(get_current_version "$service")
printf " %-20s %-12s %s\n" "$service" "$status" "$version"
done
echo
# Show recent update history
if command_exists "jq" && [[ -f "$UPDATE_STATE_FILE" ]]; then
echo "Recent Update History:"
echo "----------------------------------------"
jq -r '.update_history | reverse | .[0:5][] | "\(.timestamp | strftime("%Y-%m-%d %H:%M")) \(.service) \(.old_version)->\(.new_version) [\(.status)]"' "$UPDATE_STATE_FILE" 2>/dev/null || echo "No update history available"
fi
}
# Monitor ongoing updates
monitor_updates() {
print_info "Monitoring ongoing updates (Ctrl+C to stop)"
while true; do
clear
show_update_status
echo
echo "Press Ctrl+C to stop monitoring"
sleep 10
done
}
# =============================================================================
# AUTOMATED UPDATE FUNCTIONS
# =============================================================================
# Setup automated updates
setup_automated_updates() {
local schedule="${1:-0 3 * * 0}" # Weekly on Sunday at 3 AM
print_info "Setting up automated updates with schedule: $schedule"
# Create update script
local update_script="$HOME/.ez-homelab/update.sh"
cat > "$update_script" << EOF
#!/bin/bash
# Automated update script for EZ-Homelab
SCRIPT_DIR="$SCRIPT_DIR"
# Run updates
"\$SCRIPT_DIR/update.sh" all --quiet
# Log completion
echo "\$(date): Automated update completed" >> "$LOG_DIR/update.log"
EOF
chmod +x "$update_script"
# Add to crontab
local cron_entry="$schedule $update_script"
if ! crontab -l 2>/dev/null | grep -q "update.sh"; then
(crontab -l 2>/dev/null; echo "$cron_entry") | crontab -
print_info "Added automated updates to crontab: $cron_entry"
fi
print_success "Automated updates configured"
}
# Remove automated updates
remove_automated_updates() {
print_info "Removing automated updates"
# Remove from crontab
crontab -l 2>/dev/null | grep -v "update.sh" | crontab -
print_success "Automated updates removed"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local action=""
local service=""
local strategy="$DEFAULT_UPDATE_STRATEGY"
local schedule=""
local quiet=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << EOF
EZ-Homelab Update Management
USAGE:
update [OPTIONS] <ACTION> [SERVICE]
ACTIONS:
check Check for available updates
update Update a service or all services
status Show update status and history
monitor Monitor ongoing updates
rollback Rollback a service
schedule Setup automated updates
unschedule Remove automated updates
OPTIONS:
-s, --strategy STRATEGY Update strategy (rolling, blue-green, canary)
--schedule CRON Cron schedule for automated updates
-q, --quiet Suppress non-error output
STRATEGIES:
rolling Update one service at a time (default)
blue-green Deploy new version alongside old
canary Update subset of instances first
EXAMPLES:
update check # Check for updates
update update traefik # Update Traefik service
update update all # Update all services
update status # Show update status
update rollback traefik # Rollback Traefik
update monitor # Monitor updates
update schedule "0 3 * * 0" # Weekly updates Sunday 3 AM
EOF
exit 0
;;
-s|--strategy)
strategy="$2"
shift 2
;;
--schedule)
schedule="$2"
shift 2
;;
-q|--quiet)
quiet=true
shift
;;
check|update|status|monitor|rollback|schedule|unschedule)
action="$1"
shift
break
;;
*)
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Too many arguments"
exit 1
fi
shift
;;
esac
done
# Handle remaining arguments
while [[ $# -gt 0 ]]; do
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Too many arguments"
exit 1
fi
shift
done
# Initialize script
init_script "$SCRIPT_NAME" "$SCRIPT_VERSION"
init_logging "$SCRIPT_NAME"
init_update_state
# Check prerequisites
if ! docker_available; then
print_error "Docker is not available"
exit 1
fi
# Execute action
case "$action" in
check)
if [[ -n "$service" ]]; then
check_docker_updates "$service"
else
check_all_updates
fi
;;
update)
if [[ "$service" == "all" || -z "$service" ]]; then
update_all_services "$strategy"
else
# Get latest image for the service
local current_image
current_image=$(docker inspect "$service" --format '{{.Config.Image}}' 2>/dev/null || echo "")
if [[ -n "$current_image" ]]; then
local repo
repo=$(echo "$current_image" | cut -d: -f1)
local new_image="$repo:latest"
update_service_rolling "$service" "$new_image"
else
print_error "Cannot determine current image for service $service"
exit 1
fi
fi
;;
status)
show_update_status
;;
monitor)
monitor_updates
;;
rollback)
if [[ -n "$service" ]]; then
rollback_service "$service"
else
print_error "Service name required for rollback"
exit 1
fi
;;
schedule)
setup_automated_updates "$schedule"
;;
unschedule)
remove_automated_updates
;;
"")
print_error "No action specified. Use --help for usage information."
exit 1
;;
*)
print_error "Unknown action: $action"
exit 1
;;
esac
}
# Run main function
main "$@"

View File

@@ -0,0 +1,375 @@
#!/bin/bash
# EZ-Homelab Enhanced Setup Scripts - Multi-Purpose Validation
# Validate configurations, compose files, and deployment readiness
SCRIPT_NAME="validate"
SCRIPT_VERSION="1.0.0"
# Load common library
source "$(dirname "${BASH_SOURCE[0]}")/lib/common.sh"
source "$(dirname "${BASH_SOURCE[0]}")/lib/ui.sh"
# =============================================================================
# VALIDATION FUNCTIONS
# =============================================================================
# Validate .env file
validate_env_file() {
echo "DEBUG: Starting validate_env_file"
local env_file="$EZ_HOME/.env"
echo "DEBUG: env_file = $env_file"
if [[ ! -f "$env_file" ]]; then
echo "DEBUG: .env file not found"
return 1
fi
echo "DEBUG: .env file exists"
return 0
}
# Validate Docker Compose files
validate_compose_files() {
local service="${1:-}"
print_info "Validating Docker Compose files..."
local compose_files
if [[ -n "$service" ]]; then
compose_files=("$EZ_HOME/docker-compose/$service/docker-compose.yml")
else
mapfile -t compose_files < <(find "$EZ_HOME/docker-compose" -name "docker-compose.yml" -type f 2>/dev/null)
fi
if [[ ${#compose_files[@]} -eq 0 ]]; then
print_error "No Docker Compose files found"
return 1
fi
local errors=0
for file in "${compose_files[@]}"; do
if [[ ! -f "$file" ]]; then
print_error "Compose file not found: $file"
errors=$((errors + 1))
continue
fi
# Validate YAML syntax
if ! validate_yaml "$file"; then
print_error "Invalid YAML in $file"
errors=$((errors + 1))
continue
fi
# Validate with docker compose config
if command_exists docker && docker compose version >/dev/null 2>&1; then
if ! docker compose -f "$file" config >/dev/null 2>&1; then
print_error "Invalid Docker Compose configuration in $file"
errors=$((errors + 1))
continue
fi
fi
print_success "Validated: $file"
done
if [[ $errors -gt 0 ]]; then
print_error "Found $errors error(s) in compose files"
return 1
fi
print_success "All compose files validated"
return 0
}
# Validate Docker networks
validate_networks() {
print_info "Validating Docker networks..."
if ! docker_available; then
print_warning "Docker not available, skipping network validation"
return 2
fi
local required_networks=("traefik-network" "homelab-network")
local missing_networks=()
for network in "${required_networks[@]}"; do
if ! docker network ls --format "{{.Name}}" | grep -q "^${network}$"; then
missing_networks+=("$network")
fi
done
if [[ ${#missing_networks[@]} -gt 0 ]]; then
print_error "Missing Docker networks: ${missing_networks[*]}"
print_error "Run ./pre-deployment-wizard.sh to create networks"
return 1
fi
print_success "All required networks exist"
return 0
}
# Validate SSL certificates
validate_ssl_certificates() {
print_info "Validating SSL certificates..."
# Check if Traefik is running and has certificates
if ! docker_available; then
print_warning "Docker not available, skipping SSL validation"
return 2
fi
if ! service_running traefik 2>/dev/null; then
print_warning "Traefik not running, skipping SSL validation"
return 2
fi
# Check acme.json exists
local acme_file="$STACKS_DIR/core/traefik/acme.json"
if [[ ! -f "$acme_file" ]]; then
print_warning "SSL certificate file not found: $acme_file"
print_warning "Certificates will be obtained on first Traefik run"
return 2
fi
print_success "SSL certificate file found"
return 0
}
# Validate service dependencies
validate_service_dependencies() {
local service="${1:-}"
print_info "Validating service dependencies..."
# This is a basic implementation - could be expanded
# to check for specific service requirements
if [[ -n "$service" ]]; then
local service_dir="$EZ_HOME/docker-compose/$service"
if [[ ! -d "$service_dir" ]]; then
print_error "Service directory not found: $service_dir"
return 1
fi
local compose_file="$service_dir/docker-compose.yml"
if [[ ! -f "$compose_file" ]]; then
print_error "Compose file not found: $compose_file"
return 1
fi
print_success "Service $service dependencies validated"
else
print_success "Service dependencies validation skipped (no specific service)"
fi
return 0
}
# =============================================================================
# REPORT GENERATION
# =============================================================================
# Generate validation report
generate_validation_report() {
local report_file="$LOG_DIR/validation-report-$(date +%Y%m%d-%H%M%S).txt"
{
echo "EZ-Homelab Validation Report"
echo "============================"
echo "Date: $(date)"
echo "System: $OS_NAME $OS_VERSION ($ARCH)"
echo ""
echo "Validation Results:"
echo "- Environment: $(validate_env_file >/dev/null 2>&1 && echo "PASS" || echo "FAIL")"
echo "- Compose Files: $(validate_compose_files >/dev/null 2>&1 && echo "PASS" || echo "FAIL")"
echo "- Networks: $(validate_networks >/dev/null 2>&1; case $? in 0) echo "PASS";; 1) echo "FAIL";; 2) echo "SKIP";; esac)"
echo "- SSL Certificates: $(validate_ssl_certificates >/dev/null 2>&1; case $? in 0) echo "PASS";; 1) echo "FAIL";; 2) echo "SKIP";; esac)"
echo ""
echo "Log file: $LOG_FILE"
} > "$report_file"
print_info "Report saved to: $report_file"
}
# =============================================================================
# MAIN FUNCTION
# =============================================================================
main() {
local service=""
local check_env=true
local check_compose=true
local check_networks=true
local check_ssl=true
local non_interactive=false
local verbose=false
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << EOF
EZ-Homelab Multi-Purpose Validation
USAGE:
$SCRIPT_NAME [OPTIONS] [SERVICE]
ARGUMENTS:
SERVICE Specific service to validate (optional)
OPTIONS:
-h, --help Show this help message
-v, --verbose Enable verbose logging
--no-env Skip .env file validation
--no-compose Skip compose file validation
--no-networks Skip network validation
--no-ssl Skip SSL certificate validation
--no-ui Run without interactive UI
EXAMPLES:
$SCRIPT_NAME # Validate everything
$SCRIPT_NAME traefik # Validate only Traefik
$SCRIPT_NAME --no-ssl # Skip SSL validation
EOF
exit 0
;;
-v|--verbose)
verbose=true
;;
--no-env)
check_env=false
;;
--no-compose)
check_compose=false
;;
--no-networks)
check_networks=false
;;
--no-ssl)
check_ssl=false
;;
--no-ui)
non_interactive=true
;;
-*)
print_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
*)
if [[ -z "$service" ]]; then
service="$1"
else
print_error "Multiple services specified. Use only one service name."
exit 1
fi
;;
esac
shift
done
# Initialize script
init_script "$SCRIPT_NAME"
if $verbose; then
set -x
fi
print_info "Starting EZ-Homelab validation..."
local total_checks=0
local passed=0
local warnings=0
local failed=0
# Run validations
if $check_env; then
total_checks=$((total_checks + 1))
# Run check and capture exit code
local exit_code=0
validate_env_file || exit_code=$?
if [[ $exit_code -eq 0 ]]; then
passed=$((passed + 1))
else
failed=$((failed + 1))
fi
fi
if $check_compose; then
((total_checks++))
# Run check and capture exit code
local exit_code=0
validate_compose_files "$service" || exit_code=$?
if [[ $exit_code -eq 0 ]]; then
((passed++))
else
((failed++))
fi
fi
if $check_networks; then
total_checks=$((total_checks + 1))
# Run check and capture exit code
local exit_code=0
validate_networks || exit_code=$?
case $exit_code in
0) passed=$((passed + 1)) ;;
1) failed=$((failed + 1)) ;;
2) warnings=$((warnings + 1)) ;;
esac
fi
if $check_ssl; then
total_checks=$((total_checks + 1))
# Run check and capture exit code
local exit_code=0
validate_ssl_certificates || exit_code=$?
case $exit_code in
0) passed=$((passed + 1)) ;;
1) failed=$((failed + 1)) ;;
2) warnings=$((warnings + 1)) ;;
esac
fi
# Service-specific validation
if [[ -n "$service" ]]; then
total_checks=$((total_checks + 1))
# Run check and capture exit code
local exit_code=0
validate_service_dependencies "$service" || exit_code=$?
if [[ $exit_code -eq 0 ]]; then
passed=$((passed + 1))
else
failed=$((failed + 1))
fi
fi
echo ""
print_info "Validation complete: $passed passed, $warnings warnings, $failed failed"
# Generate report
generate_validation_report
# Determine exit code
if [[ $failed -gt 0 ]]; then
print_error "Validation failed. Check the log file: $LOG_FILE"
exit 1
elif [[ $warnings -gt 0 ]]; then
print_warning "Validation passed with warnings"
exit 2
else
print_success "All validations passed!"
exit 0
fi
}
# Run main function
main "$@"