450 lines
14 KiB
Bash
Executable File
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
|
|
}
|
|
|