438 lines
16 KiB
Bash
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 |