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:
686
scripts/enhanced-setup/backup.sh
Executable file
686
scripts/enhanced-setup/backup.sh
Executable 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
440
scripts/enhanced-setup/deploy.sh
Executable 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 "$@"
|
||||
399
scripts/enhanced-setup/generalize.sh
Executable file
399
scripts/enhanced-setup/generalize.sh
Executable 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 "$@"
|
||||
373
scripts/enhanced-setup/lib/common.sh
Executable file
373
scripts/enhanced-setup/lib/common.sh
Executable 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
324
scripts/enhanced-setup/lib/ui.sh
Executable 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
|
||||
}
|
||||
296
scripts/enhanced-setup/localize.sh
Executable file
296
scripts/enhanced-setup/localize.sh
Executable 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
577
scripts/enhanced-setup/monitor.sh
Executable 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 "$@"
|
||||
352
scripts/enhanced-setup/prd.md
Normal file
352
scripts/enhanced-setup/prd.md
Normal 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.
|
||||
372
scripts/enhanced-setup/pre-deployment-wizard.sh
Executable file
372
scripts/enhanced-setup/pre-deployment-wizard.sh
Executable 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 "$@"
|
||||
373
scripts/enhanced-setup/preflight.sh
Executable file
373
scripts/enhanced-setup/preflight.sh
Executable 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
556
scripts/enhanced-setup/service.sh
Executable 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
383
scripts/enhanced-setup/setup.sh
Executable 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 "$@"
|
||||
150
scripts/enhanced-setup/standards.md
Normal file
150
scripts/enhanced-setup/standards.md
Normal 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
600
scripts/enhanced-setup/update.sh
Executable 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 "$@"
|
||||
375
scripts/enhanced-setup/validate.sh
Executable file
375
scripts/enhanced-setup/validate.sh
Executable 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 "$@"
|
||||
Reference in New Issue
Block a user