homelab/ansible/ansible-old/scripts/day0bootstrap.sh

438 lines
16 KiB
Bash

#!/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 <ip> [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" <<EOF
# ==============================================================================
# Ansible Inventory (Generated by day0bootstrap.sh)
# ==============================================================================
[proxmox_nodes]
proxmox_server \
ansible_host=$PROXMOX_IP \
ansible_user=$PROXMOX_USER \
ansible_ssh_private_key_file=$SSH_KEY_PATH \
ansible_port=$PROXMOX_PORT
[proxmox_nodes:vars]
ansible_python_interpreter=/usr/bin/python3
EOF
chmod 644 "$INVENTORY_FILE"
log_success "Created new inventory file: $INVENTORY_FILE"
fi
# ==============================================================================
# SECTION 7: VERIFICATION
# ==============================================================================
log_step "VERIFICATION"
log_warning "Testing Ansible connectivity..."
if [ "$INTEGRATION_REQUIRED" = "true" ] && [ "$INVENTORY_HAS_TARGET" != "true" ]; then
log_warning "Skipped Ansible ping for new host: add the suggested entry to $ACTIVE_INVENTORY first"
echo ""
echo "Verification command after updating inventory:"
echo "ansible $TARGET_GROUP -i $ACTIVE_INVENTORY -m ping"
echo ""
echo "============================================"
echo "✓ BOOTSTRAP COMPLETE (PENDING INVENTORY ADD)"
echo "============================================"
echo ""
exit 0
fi
if ANSIBLE_OUT=$(ansible "$TARGET_GROUP" -i "$ACTIVE_INVENTORY" -m ping 2>&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 <your-playbook>.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