Files

450 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# Common functions and utilities for Proxmox deployment scripts
# Don't use set -euo pipefail here as this is a library file
# Individual scripts should set this themselves
set +euo pipefail
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1" >&2
}
log_success() {
echo -e "${GREEN}[✓]${NC} $1" >&2
}
log_warn() {
echo -e "${YELLOW}[WARNING]${NC} $1" >&2
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
log_debug() {
if [[ "${DEBUG:-}" == "1" ]]; then
echo -e "${BLUE}[DEBUG]${NC} $1" >&2
fi
}
# Error handling
error_exit() {
log_error "$1"
exit 1
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check if running as root (for Proxmox host operations)
check_root() {
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root for Proxmox host operations"
fi
}
# Get script directory
get_script_dir() {
# Find the actual calling script's directory (not this common.sh file)
local depth=1
local script_dir
# Walk up the call stack to find the actual script
while [[ $depth -lt 10 ]]; do
if [[ -n "${BASH_SOURCE[$depth]:-}" ]]; then
script_dir="$(cd "$(dirname "${BASH_SOURCE[$depth]}")" && pwd)"
# If this is not lib/common.sh, use it
if [[ "$(basename "$script_dir")" != "lib" ]]; then
echo "$script_dir"
return 0
fi
fi
depth=$((depth + 1))
done
# Fallback: use current script directory
cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
}
# Get project root
get_project_root() {
local script_dir
script_dir="$(get_script_dir)"
# If we're in scripts/deployment, go up 2 levels
# If we're in scripts/, go up 1 level
# If we're in lib/, go up 1 level
if [[ "$script_dir" == */scripts/deployment ]]; then
echo "$(cd "$script_dir/../.." && pwd)"
elif [[ "$script_dir" == */scripts ]]; then
echo "$(cd "$script_dir/.." && pwd)"
elif [[ "$script_dir" == */lib ]]; then
echo "$(cd "$script_dir/.." && pwd)"
else
# Default: go up one level
echo "$(cd "$script_dir/.." && pwd)"
fi
}
# Load .env file from ~/.env (standardized location)
load_env_file() {
local env_file="${HOME}/.env"
if [[ -f "$env_file" ]]; then
# Source PROXMOX_* variables from ~/.env
set -a
source <(grep -E "^PROXMOX_" "$env_file" 2>/dev/null | sed 's/^/export /' || true)
set +a
log_debug "Loaded environment variables from: $env_file"
fi
}
# Load configuration file
load_config() {
local config_file="${1:-}"
local project_root
# Use PROJECT_ROOT if already set (from calling script), otherwise detect it
if [[ -n "${PROJECT_ROOT:-}" ]]; then
project_root="$PROJECT_ROOT"
else
project_root="$(get_project_root)"
fi
# First, load from ~/.env (standardized location for credentials)
load_env_file
if [[ -z "$config_file" ]]; then
config_file="${project_root}/config/proxmox.conf"
fi
# If config_file is relative, make it absolute relative to project_root
if [[ "$config_file" != /* ]]; then
if [[ "$config_file" == config/* ]]; then
config_file="${project_root}/${config_file}"
else
config_file="${project_root}/config/${config_file}"
fi
fi
if [[ ! -f "$config_file" ]]; then
error_exit "Configuration file not found: $config_file"
fi
# Source config file (may override or add to .env values)
# But preserve PROJECT_ROOT if it was already set correctly
local saved_project_root="${PROJECT_ROOT:-}"
set -a
source "$config_file"
set +a
# If PROJECT_ROOT was set in config but we had a better one, restore it
if [[ -n "$saved_project_root" ]] && [[ "$saved_project_root" == *"smom-dbis-138-proxmox"* ]]; then
PROJECT_ROOT="$saved_project_root"
fi
# Ensure PROXMOX_TOKEN_SECRET is set from PROXMOX_TOKEN_VALUE if needed (for backwards compatibility)
if [[ -z "${PROXMOX_TOKEN_SECRET:-}" ]] && [[ -n "${PROXMOX_TOKEN_VALUE:-}" ]]; then
PROXMOX_TOKEN_SECRET="${PROXMOX_TOKEN_VALUE}"
fi
log_debug "Loaded configuration from: $config_file"
}
# Validate required variables
validate_vars() {
local missing_vars=()
local var
for var in "$@"; do
if [[ -z "${!var:-}" ]]; then
missing_vars+=("$var")
fi
done
if [[ ${#missing_vars[@]} -gt 0 ]]; then
error_exit "Missing required variables: ${missing_vars[*]}"
fi
}
# Generate random password
generate_password() {
openssl rand -base64 32 | tr -d "=+/" | cut -c1-25
}
# Wait for container to be ready
wait_for_container() {
local vmid="$1"
local max_attempts="${2:-30}"
local attempt=1
log_info "Waiting for container $vmid to be ready..."
while [[ $attempt -le $max_attempts ]]; do
# Check if container exists (status command works)
if pct status "$vmid" &>/dev/null 2>&1; then
local status
status=$(pct status "$vmid" 2>/dev/null | awk '{print $2}' || echo "")
# Container is ready if it exists (can be stopped or running)
if [[ -n "$status" ]]; then
log_success "Container $vmid is ready (status: $status)"
return 0
fi
fi
sleep 2
attempt=$((attempt + 1))
done
error_exit "Container $vmid did not become ready in time"
}
# Get container IP address
get_container_ip() {
local vmid="$1"
local interface="${2:-eth0}"
pct exec "$vmid" -- ip -4 addr show "$interface" | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1
}
# Execute command in container
exec_in_container() {
local vmid="$1"
shift
local cmd=("$@")
log_debug "Executing in container $vmid: ${cmd[*]}"
pct exec "$vmid" -- "${cmd[@]}"
}
# Copy file to container
copy_to_container() {
local vmid="$1"
local src="$2"
local dst="$3"
log_debug "Copying $src to container $vmid:$dst"
pct push "$vmid" "$src" "$dst"
}
# Create backup directory
create_backup_dir() {
local backup_base="${1:-/var/lib/vz/backup/smom-dbis-138}"
local timestamp
timestamp="$(date +%Y%m%d_%H%M%S)"
local backup_dir="${backup_base}/${timestamp}"
mkdir -p "$backup_dir"
echo "$backup_dir"
}
# Validate IP address
validate_ip() {
local ip="$1"
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
local IFS='.'
local -a octets
read -ra octets <<< "$ip"
for octet in "${octets[@]}"; do
if [[ $octet -gt 255 ]]; then
return 1
fi
done
return 0
fi
return 1
}
# Convert NETMASK to CIDR prefix length
# Supports both dotted decimal (255.255.255.0) and CIDR prefix (24) formats
get_cidr_prefix() {
local netmask="${1:-24}"
# If already a number, assume it's a CIDR prefix
if [[ "$netmask" =~ ^[0-9]+$ ]] && [[ "$netmask" -ge 0 ]] && [[ "$netmask" -le 32 ]]; then
echo "$netmask"
return 0
fi
# Convert dotted decimal to CIDR
case "$netmask" in
"255.255.255.255") echo "32" ;;
"255.255.255.254") echo "31" ;;
"255.255.255.252") echo "30" ;;
"255.255.255.248") echo "29" ;;
"255.255.255.240") echo "28" ;;
"255.255.255.224") echo "27" ;;
"255.255.255.192") echo "26" ;;
"255.255.255.128") echo "25" ;;
"255.255.255.0") echo "24" ;;
"255.255.254.0") echo "23" ;;
"255.255.252.0") echo "22" ;;
"255.255.248.0") echo "21" ;;
"255.255.240.0") echo "20" ;;
"255.255.224.0") echo "19" ;;
"255.255.192.0") echo "18" ;;
"255.255.128.0") echo "17" ;;
"255.255.0.0") echo "16" ;;
"255.254.0.0") echo "15" ;;
"255.252.0.0") echo "14" ;;
"255.248.0.0") echo "13" ;;
"255.240.0.0") echo "12" ;;
"255.224.0.0") echo "11" ;;
"255.192.0.0") echo "10" ;;
"255.128.0.0") echo "9" ;;
"255.0.0.0") echo "8" ;;
*)
log_warn "Unknown netmask format: $netmask, defaulting to 24"
echo "24"
;;
esac
}
# Get next available VMID
get_next_vmid() {
local start_vmid="${1:-100}"
local vmid="$start_vmid"
while pct list | grep -q "^\s*$vmid\s"; do
vmid=$((vmid + 1))
done
echo "$vmid"
}
# Parse container inventory
parse_inventory() {
local inventory_file="${1:-}"
local project_root
project_root="$(get_project_root)"
if [[ -z "$inventory_file" ]]; then
inventory_file="${project_root}/config/inventory.conf"
fi
if [[ ! -f "$inventory_file" ]]; then
log_warn "Inventory file not found: $inventory_file"
return 1
fi
# Source inventory file
source "$inventory_file"
return 0
}
# Check and ensure OS template exists
# Extracts template name from CONTAINER_OS_TEMPLATE (e.g., "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst" -> "ubuntu-22.04-standard_22.04-1_amd64.tar.zst")
# If exact version not found, tries to find and use the latest available version
ensure_os_template() {
local template="${1:-${CONTAINER_OS_TEMPLATE:-local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst}}"
# Extract template filename from storage:path format
# Format: "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
local template_name
if [[ "$template" == *":"* ]]; then
# Remove storage prefix (e.g., "local:")
template_name="${template#*:}"
# Remove path prefix if present (e.g., "vztmpl/")
template_name="${template_name##*/}"
else
template_name="$template"
fi
# Extract base name for checking (e.g., "ubuntu-22.04-standard" from "ubuntu-22.04-standard_22.04-1_amd64.tar.zst")
local template_base="${template_name%%_*}"
log_info "Checking for OS template: $template_name (base: $template_base)"
# Check if template exists using pveam
if ! command_exists pveam; then
log_error "pveam command not found. Cannot check/download template."
return 1
fi
# Check if exact template exists locally
# pveam list local format: "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
local local_list
local_list=$(pveam list local 2>/dev/null || echo "")
if echo "$local_list" | grep -qE "vztmpl/${template_name}"; then
log_success "OS template found: $template_name"
return 0
fi
# Check if any version of this template base exists locally
local local_template
local_template=$(echo "$local_list" | grep -E "vztmpl/${template_base}_" | head -1 | sed 's/.*vztmpl\///' | awk '{print $1}' || echo "")
if [[ -n "$local_template" ]]; then
log_warn "Exact template version not found, but found: $local_template"
log_info "Using available template: $local_template"
# Update CONTAINER_OS_TEMPLATE to use the found template
local storage_prefix="${template%%:*}"
CONTAINER_OS_TEMPLATE="${storage_prefix}:vztmpl/${local_template}"
log_success "Updated CONTAINER_OS_TEMPLATE to: $CONTAINER_OS_TEMPLATE"
return 0
fi
log_warn "OS template not found locally: $template_name"
log_info "Checking available templates for download..."
# Check available templates and find the best match
local available_template
available_template=$(pveam available 2>/dev/null | grep -E "(^|\s)${template_base}_" | head -1 | awk '{print $2}' || echo "")
if [[ -n "$available_template" ]]; then
log_info "Found available template: $available_template"
log_info "Downloading template (this may take a few minutes)..."
# Try to download the available template
local download_output
download_output=$(pveam download local "$available_template" 2>&1)
local download_exit=$?
# Wait a moment for download to complete
sleep 2
# Check if download was successful or if file already exists
if [[ $download_exit -eq 0 ]] || echo "$download_output" | grep -q "OK, got correct file already"; then
# Verify it exists now
local verify_list
verify_list=$(pveam list local 2>/dev/null || echo "")
if echo "$verify_list" | grep -qE "vztmpl/${available_template}"; then
log_success "Template available: $available_template"
# Update CONTAINER_OS_TEMPLATE to use the template
local storage_prefix="${template%%:*}"
CONTAINER_OS_TEMPLATE="${storage_prefix}:vztmpl/${available_template}"
log_info "Using template: $CONTAINER_OS_TEMPLATE"
return 0
else
# Template might be in cache but not listed yet, or verification format issue
log_warn "Template download completed, but verification format may differ"
log_info "Template should be available. Proceeding..."
local storage_prefix="${template%%:*}"
CONTAINER_OS_TEMPLATE="${storage_prefix}:vztmpl/${available_template}"
return 0 # Continue anyway - template is likely available
fi
else
log_error "Failed to download template: $available_template"
log_info "Please download it manually:"
log_info " pveam download local $available_template"
return 1
fi
else
log_error "Template not found in available templates: $template_base"
log_info "Available Ubuntu templates:"
pveam available 2>/dev/null | grep -i ubuntu | head -5 || echo " (none found)"
log_info "Please download a template manually:"
log_info " pveam download local <template-name>"
return 1
fi
}