#!/bin/bash # ============================================================================== # LEAD ARCHITECT — DAY 0 BOOTSTRAP # Pre-Ansible Environment Setup Script (Debian/Ubuntu) # Purpose: Prepare a fresh host for Ansible management # ============================================================================== set -euo pipefail # Exit on error, undefined vars, pipe failures # ============================================================================== # CONFIGURATION # ============================================================================== PROXMOX_IP="${1:-}" # pass as arg1 or prompt interactively PROXMOX_USER="${2:-root}" # <--- CHANGE ME or pass as arg2 PROXMOX_HOSTNAME="${3:-}" # <--- OPTIONAL: pass as arg3 PROXMOX_PORT="${4:-22}" # <--- OPTIONAL: SSH port # Always resolve paths from script location for deterministic behavior. SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" ANSIBLE_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" SSH_KEY_PATH="$HOME/.ssh/id_ed25519" KNOWN_HOSTS="$HOME/.ssh/known_hosts" INVENTORY_FILE="${INVENTORY_PATH:-$ANSIBLE_ROOT/inventory/hosts.ini}" ACTIVE_INVENTORY="$INVENTORY_FILE" TARGET_GROUP="proxmox_nodes" INTEGRATION_REQUIRED="false" INVENTORY_HAS_TARGET="false" EFFECTIVE_VERIFY_USER="$PROXMOX_USER" EXISTING_ALIAS="" # Prompt for IP when not provided (interactive shells only). if [ -z "$PROXMOX_IP" ]; then if [ -t 0 ]; then read -r -p "Enter Proxmox host IP address: " PROXMOX_IP else log_error "PROXMOX_IP is required in non-interactive mode. Usage: ./day0bootstrap.sh [user] [hostname] [port]" exit 1 fi fi # Validate IPv4 format to fail early on typos. if ! [[ "$PROXMOX_IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then log_error "Invalid IPv4 address format: $PROXMOX_IP" exit 1 fi IFS='.' read -r -a _ip_octets <<< "$PROXMOX_IP" for _octet in "${_ip_octets[@]}"; do if [ "$_octet" -lt 0 ] || [ "$_octet" -gt 255 ]; then log_error "Invalid IPv4 octet in address: $PROXMOX_IP" exit 1 fi done # Logging functions log_step() { echo "" echo "[⚙ STEP] $1" } log_success() { echo "[✓ OK] $1" } log_error() { echo "[✗ ERROR] $1" >&2 } log_warning() { echo "[⚠ WARNING] $1" } # Error trap trap 'log_error "Bootstrap failed at line $LINENO"; exit 1' ERR # ============================================================================== # SECTION 1: PRE-FLIGHT VALIDATION # ============================================================================== log_step "PRE-FLIGHT VALIDATION" # 1.1 Verify target is reachable if ! ping -c 1 -W 2 "$PROXMOX_IP" &>/dev/null; then log_error "Cannot reach $PROXMOX_IP. Check network and IP address." exit 1 fi log_success "Host $PROXMOX_IP is reachable" # 1.2 Check if we can resolve target hostname if [ -z "$PROXMOX_HOSTNAME" ]; then if PROXMOX_HOSTNAME=$(getent hosts "$PROXMOX_IP" | awk '{print $2}'); then log_success "Resolved hostname from /etc/hosts: $PROXMOX_HOSTNAME" else PROXMOX_HOSTNAME="proxmox-${PROXMOX_IP##*.}" log_warning "No hostname resolved; using fallback alias: $PROXMOX_HOSTNAME" fi else log_success "Using provided hostname: $PROXMOX_HOSTNAME" fi # 1.3 Detect local IP (for context) LOCAL_IP=$(hostname -I | awk '{print $1}') log_success "Local IP detected: $LOCAL_IP" # ============================================================================== # SECTION 2: SSH KEY MANAGEMENT # ============================================================================== log_step "SSH KEY MANAGEMENT" # 2.1 Create .ssh directory if needed mkdir -p "$HOME/.ssh" chmod 700 "$HOME/.ssh" log_success "SSH directory ready: $HOME/.ssh" # 2.2 Check for existing keys (ED25519 > RSA preference) if [ -f "$SSH_KEY_PATH" ]; then log_success "Found existing ED25519 key at $SSH_KEY_PATH" elif [ -f "$HOME/.ssh/id_rsa" ]; then SSH_KEY_PATH="$HOME/.ssh/id_rsa" log_success "Found existing RSA key; using as fallback: $SSH_KEY_PATH" else log_warning "No SSH key found. Generating new ED25519 keypair..." ssh-keygen -t ed25519 -f "$SSH_KEY_PATH" -N "" -C "ansible@$(hostname)" chmod 600 "$SSH_KEY_PATH" chmod 644 "${SSH_KEY_PATH}.pub" log_success "Generated new key: $SSH_KEY_PATH" fi # ============================================================================== # SECTION 3: SSH TRUST (Option A: ssh-keyscan) # ============================================================================== log_step "SSH TRUST ESTABLISHMENT (ssh-keyscan)" # 3.1 Create known_hosts if missing if [ ! -f "$KNOWN_HOSTS" ]; then touch "$KNOWN_HOSTS" chmod 600 "$KNOWN_HOSTS" log_success "Created new $KNOWN_HOSTS" fi # 3.2 Remove old host key for this IP (if exists) to avoid conflicts if grep -q "^$PROXMOX_IP " "$KNOWN_HOSTS" 2>/dev/null; then log_warning "Removing outdated host key for $PROXMOX_IP from known_hosts..." ssh-keygen -f "$KNOWN_HOSTS" -R "$PROXMOX_IP" >/dev/null 2>&1 || true fi # 3.3 Scan and add new host key log_warning "Scanning remote host key (this may take a few seconds)..." if ssh-keyscan -p "$PROXMOX_PORT" -H "$PROXMOX_IP" >> "$KNOWN_HOSTS" 2>/dev/null; then log_success "Host key added to known_hosts" else log_error "Failed to scan host key. Verify target is running SSH." exit 1 fi # 3.4 Transfer public key via ssh-copy-id log_warning "Transferring SSH public key to $PROXMOX_USER@$PROXMOX_IP..." if ssh-copy-id -i "${SSH_KEY_PATH}.pub" \ -o StrictHostKeyChecking=accept-new \ -o ConnectTimeout=5 \ -p "$PROXMOX_PORT" \ "${PROXMOX_USER}@${PROXMOX_IP}" 2>&1; then log_success "Public key installed on remote host" else log_error "Failed to copy public key. Verify SSH credentials and connectivity." exit 1 fi # ============================================================================== # SECTION 4: PACKAGE INSTALLATION # ============================================================================== log_step "PACKAGE INSTALLATION (Debian/Ubuntu)" # 4.1 Update package lists log_warning "Updating package lists..." sudo apt-get update -qq log_success "Package lists updated" # 4.2 Install Ansible (skip if already installed) if command -v ansible &>/dev/null; then ANSIBLE_VERSION=$(ansible --version | head -n1) log_success "Ansible already installed: $ANSIBLE_VERSION" else log_warning "Installing Ansible..." sudo apt-get install -y -qq ansible log_success "Ansible installed" fi # 4.3 Install Python3-pip (skip if present) if command -v pip3 &>/dev/null; then log_success "python3-pip already installed" else log_warning "Installing python3-pip..." sudo apt-get install -y -qq python3-pip log_success "python3-pip installed" fi # 4.4 Install additional tools (git, curl, jq) TOOLS=("git" "curl" "jq") for tool in "${TOOLS[@]}"; do if command -v "$tool" &>/dev/null; then log_success "$tool already installed" else log_warning "Installing $tool..." sudo apt-get install -y -qq "$tool" log_success "$tool installed" fi done # 4.5 Install Proxmoxer (Python library) log_warning "Installing Proxmoxer (Python library)..." if pip3 install --quiet proxmoxer 2>/dev/null; then log_success "Proxmoxer installed (user-level)" else log_warning "Proxmoxer install had warnings (may already exist)" fi # ============================================================================== # SECTION 5: NTP TIME SYNCHRONIZATION # ============================================================================== log_step "NTP TIME SYNCHRONIZATION" # 5.1 Check timedatectl status if command -v timedatectl &>/dev/null; then if timedatectl status | grep -q "synchronized: yes"; then log_success "NTP is synchronized" else log_warning "NTP not synchronized. Attempting sync..." sudo timedatectl set-ntp true 2>/dev/null || true sleep 2 if timedatectl status | grep -q "synchronized: yes"; then log_success "NTP synchronized" else log_warning "NTP sync pending; SSH key negotiation may fail if time drift is excessive" fi fi else log_warning "timedatectl not available; skipping NTP check" fi # ============================================================================== # SECTION 6: INVENTORY GENERATION # ============================================================================== log_step "INVENTORY GENERATION" # 6.1 Use one canonical inventory path for consistent behavior EXISTING_INVENTORY="" if [ -f "$INVENTORY_FILE" ]; then EXISTING_INVENTORY="$INVENTORY_FILE" log_success "Found canonical inventory: $EXISTING_INVENTORY" else log_warning "Canonical inventory not found; will create: $INVENTORY_FILE" fi # 6.2 Handle existing inventory if [ -n "$EXISTING_INVENTORY" ]; then ACTIVE_INVENTORY="$EXISTING_INVENTORY" INTEGRATION_REQUIRED="true" log_warning "Existing inventory detected at: $EXISTING_INVENTORY" echo "" echo "============================================" echo "⚠ MANUAL CONFIGURATION REQUIRED" echo "============================================" echo "" echo "To integrate this host into your Ansible inventory," echo "ADD the following entry to: $EXISTING_INVENTORY" echo "" echo "--- SUGGESTED ADDITION ---" echo "" # Detect group context from existing inventory if grep -q "\[proxmox" "$EXISTING_INVENTORY"; then GROUP="proxmox_cluster" TARGET_GROUP="$GROUP" # If target IP already exists in inventory, don't suggest duplicate add. if grep -Eq "^[[:space:]]*[^#[:space:]]+[[:space:]]+ansible_host=${PROXMOX_IP}([[:space:]]|$)" "$EXISTING_INVENTORY"; then INVENTORY_HAS_TARGET="true" EXISTING_ALIAS=$(grep -E "^[[:space:]]*[^#[:space:]]+[[:space:]]+ansible_host=${PROXMOX_IP}([[:space:]]|$)" "$EXISTING_INVENTORY" | awk '{print $1}' | head -n1) log_success "Target IP already exists in inventory as host: ${EXISTING_ALIAS}" # Resolve effective SSH user from host line or group vars section. HOST_LINE=$(grep -E "^[[:space:]]*${EXISTING_ALIAS}[[:space:]]+" "$EXISTING_INVENTORY" | head -n1 || true) HOST_USER=$(echo "$HOST_LINE" | sed -nE 's/.*ansible_user=([^[:space:]]+).*/\1/p') GROUP_USER=$(awk -v section="[$GROUP:vars]" ' $0==section {in_section=1; next} /^\[/ && in_section {exit} in_section && $0 ~ /^ansible_user=/ { gsub(/^ansible_user=/, "", $0) print $0 exit } ' "$EXISTING_INVENTORY") if [ -n "$HOST_USER" ]; then EFFECTIVE_VERIFY_USER="$HOST_USER" elif [ -n "$GROUP_USER" ]; then EFFECTIVE_VERIFY_USER="$GROUP_USER" fi if [ "$EFFECTIVE_VERIFY_USER" != "$PROXMOX_USER" ]; then log_warning "Inventory user ($EFFECTIVE_VERIFY_USER) differs from bootstrap SSH user ($PROXMOX_USER)" fi echo "# No addition required: host already exists in [proxmox_cluster]" echo "# Existing entry alias: ${EXISTING_ALIAS}" echo "" echo "COPY/PASTE BLOCK" echo "(No inventory update needed for this host)" else # Infer naming style: pveNN sequence if present. if grep -Eq "^[[:space:]]*pve[0-9]+[[:space:]]+ansible_host=" "$EXISTING_INVENTORY"; then NEXT_NUM=$(grep -E "^[[:space:]]*pve[0-9]+[[:space:]]+ansible_host=" "$EXISTING_INVENTORY" | sed -E 's/^[[:space:]]*pve([0-9]+).*/\1/' | sort -n | tail -n1) if [ -n "$NEXT_NUM" ]; then PROXMOX_HOSTNAME="pve$(printf "%02d" $((10#$NEXT_NUM + 1)))" fi fi echo "# Add to the [proxmox_cluster] section:" echo "" echo "COPY/PASTE BLOCK" echo "-----8<-----" echo "$PROXMOX_HOSTNAME ansible_host=$PROXMOX_IP ansible_user=$PROXMOX_USER ansible_ssh_private_key_file=$SSH_KEY_PATH ansible_port=$PROXMOX_PORT" echo "----->8-----" fi else GROUP="proxmox_nodes" TARGET_GROUP="$GROUP" echo "# Add a new section for Proxmox nodes:" echo "" echo "COPY/PASTE BLOCK" echo "-----8<-----" echo "[$GROUP]" echo "$PROXMOX_HOSTNAME ansible_host=$PROXMOX_IP ansible_user=$PROXMOX_USER ansible_ssh_private_key_file=$SSH_KEY_PATH ansible_port=$PROXMOX_PORT" echo "" echo "[$GROUP:vars]" echo "ansible_python_interpreter=/usr/bin/python3" echo "----->8-----" fi echo "" echo "--- END SUGGESTION ---" echo "" echo "Current inventory file excerpt:" echo "-------------------------------" grep -E "^\[|^[a-zA-Z0-9_-]+ " "$EXISTING_INVENTORY" | head -20 echo "" else # 6.3 Create new inventory file if none exists log_warning "No existing inventory found. Creating $INVENTORY_FILE..." mkdir -p "$(dirname "$INVENTORY_FILE")" cat > "$INVENTORY_FILE" <&1); then log_success "Ansible verifies connectivity to target host!" echo "" echo "============================================" echo "✓ BOOTSTRAP COMPLETE" echo "============================================" echo "" echo "Next steps:" echo "1. Review $ACTIVE_INVENTORY" echo "2. Run your Ansible playbook:" echo " ansible-playbook -i $ACTIVE_INVENTORY .yml" echo "" else log_warning "Inventory-based ping failed. Collecting diagnostics..." echo "$ANSIBLE_OUT" # If existing inventory already has host, test with bootstrap credentials directly. if [ "$INVENTORY_HAS_TARGET" = "true" ]; then log_warning "Trying direct host ping with bootstrap credentials ($PROXMOX_USER)..." if ansible all -i "${PROXMOX_IP}," -u "$PROXMOX_USER" --private-key "$SSH_KEY_PATH" -m ping >/dev/null 2>&1; then log_success "Direct SSH ping with bootstrap user works" echo "" echo "============================================" echo "✓ BOOTSTRAP COMPLETE (INVENTORY USER MISMATCH)" echo "============================================" echo "" echo "Inventory authentication does not match bootstrap credentials." echo "Current inventory likely uses: ansible_user=$EFFECTIVE_VERIFY_USER" echo "Bootstrap validated with: ansible_user=$PROXMOX_USER" echo "" echo "Suggested fixes (pick one):" echo "1. Update host entry '$EXISTING_ALIAS' in $ACTIVE_INVENTORY with ansible_user=$PROXMOX_USER" echo "2. Add your SSH key for user '$EFFECTIVE_VERIFY_USER' on the target host" echo "3. Temporarily run playbooks with override: -u $PROXMOX_USER" echo "" exit 0 fi fi log_error "Ansible ping test failed and direct bootstrap-user test also failed" exit 1 fi