BREAKING CHANGE: day0bootstrap.sh deprecated in favor of bootstrap.sh - Add scripts/bootstrap.sh (488 lines): Unified entrypoint supporting multiple hardware types (Proxmox/Docker VMs/Pi) - Create scripts/lib/ modular library system: - detection.sh: OS/hardware/container detection (362 lines) - fingerprint.sh: System fingerprinting and inventory (494 lines) - network.sh: IP configuration and VLAN placement (356 lines) - proxmox.sh: PVE post-install automation (453 lines) - validation.sh: Comprehensive pre-flight checks (510 lines) - Add validation tools: validate-node.sh, onboarding.sh, pi_init.sh - Deprecate scripts/day0bootstrap.sh with graceful redirect wrapper - Document architecture in scripts/README.md (495 lines) and PROXMOX-COMPARISON.md - Update SOP-002 with new bootstrap workflow - Add nodes/watchtower/compose.yaml (Raspberry Pi 5 stack) Migration: Existing day0bootstrap.sh users automatically redirected to new system after 5-second warning. No manual intervention required. Ref: Infrastructure automation modernization per active-tasks.md
495 lines
14 KiB
Bash
495 lines
14 KiB
Bash
#!/bin/bash
|
|
|
|
# ==============================================================================
|
|
# FINGERPRINT LIBRARY: Hardware Inventory Collection
|
|
# ==============================================================================
|
|
# Part of unified bootstrap system for homelab infrastructure
|
|
# Collects comprehensive hardware facts and generates structured YAML output
|
|
# compatible with gather_hardware_facts.yml playbook format.
|
|
# ==============================================================================
|
|
|
|
# Source detection library if not already loaded
|
|
if ! type -t detect_os_family &>/dev/null; then
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=./detection.sh
|
|
source "${SCRIPT_DIR}/detection.sh"
|
|
fi
|
|
|
|
# --- HARDWARE FACT COLLECTION ---
|
|
|
|
collect_cpu_facts() {
|
|
# Collect CPU information in structured format
|
|
# Returns JSON-like structured data
|
|
|
|
local cpu_model=$(grep "model name" /proc/cpuinfo | head -n1 | cut -d: -f2 | xargs)
|
|
local cpu_cores=$(nproc 2>/dev/null || echo "unknown")
|
|
local cpu_vendor=$(detect_cpu_vendor)
|
|
local cpu_gen=$(detect_cpu_generation)
|
|
local cpu_arch=$(uname -m)
|
|
|
|
# Get CPU frequency (current)
|
|
local cpu_freq_mhz=$(grep "cpu MHz" /proc/cpuinfo | head -n1 | awk '{print $4}' | cut -d. -f1)
|
|
|
|
# Get CPU max frequency if available
|
|
local cpu_max_freq="unknown"
|
|
if [ -f /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq ]; then
|
|
local freq_khz=$(cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq)
|
|
cpu_max_freq=$((freq_khz / 1000))
|
|
fi
|
|
|
|
cat <<EOF
|
|
cpu:
|
|
model: "$cpu_model"
|
|
vendor: "$cpu_vendor"
|
|
architecture: "$cpu_arch"
|
|
generation: "$cpu_gen"
|
|
cores: $cpu_cores
|
|
current_freq_mhz: ${cpu_freq_mhz:-unknown}
|
|
max_freq_mhz: $cpu_max_freq
|
|
EOF
|
|
}
|
|
|
|
collect_memory_facts() {
|
|
# Collect memory information
|
|
|
|
local mem_total_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
|
local mem_total_mb=$((mem_total_kb / 1024))
|
|
local mem_total_gb=$((mem_total_kb / 1024 / 1024))
|
|
|
|
local mem_free_kb=$(grep MemFree /proc/meminfo | awk '{print $2}')
|
|
local mem_free_mb=$((mem_free_kb / 1024))
|
|
|
|
local swap_total_kb=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
|
|
local swap_total_mb=$((swap_total_kb / 1024))
|
|
|
|
cat <<EOF
|
|
memory:
|
|
total_mb: $mem_total_mb
|
|
total_gb: $mem_total_gb
|
|
free_mb: $mem_free_mb
|
|
swap_mb: $swap_total_mb
|
|
EOF
|
|
}
|
|
|
|
collect_disk_facts() {
|
|
# Collect disk/storage information
|
|
|
|
cat <<EOF
|
|
storage:
|
|
EOF
|
|
|
|
# Root partition
|
|
local root_total=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
|
local root_used=$(df -BG / | awk 'NR==2 {print $3}' | sed 's/G//')
|
|
local root_avail=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
|
|
local root_device=$(df / | awk 'NR==2 {print $1}')
|
|
|
|
cat <<EOF
|
|
root:
|
|
device: "$root_device"
|
|
total_gb: $root_total
|
|
used_gb: $root_used
|
|
available_gb: $root_avail
|
|
EOF
|
|
|
|
# Detect disk type (SSD vs HDD)
|
|
local device_name=$(echo "$root_device" | sed 's|/dev/||' | sed 's/[0-9]*$//')
|
|
local disk_type="unknown"
|
|
|
|
if [ -f "/sys/block/$device_name/queue/rotational" ]; then
|
|
local rotational=$(cat "/sys/block/$device_name/queue/rotational")
|
|
if [ "$rotational" -eq 0 ]; then
|
|
disk_type="ssd"
|
|
else
|
|
disk_type="hdd"
|
|
fi
|
|
fi
|
|
|
|
cat <<EOF
|
|
type: "$disk_type"
|
|
EOF
|
|
|
|
# List all block devices
|
|
if command -v lsblk &>/dev/null; then
|
|
local block_devices=$(lsblk -d -n -o NAME,SIZE,TYPE | grep disk | awk '{printf " - { name: \"%s\", size: \"%s\" }\n", $1, $2}')
|
|
if [ -n "$block_devices" ]; then
|
|
cat <<EOF
|
|
block_devices:
|
|
$block_devices
|
|
EOF
|
|
fi
|
|
fi
|
|
}
|
|
|
|
collect_network_facts() {
|
|
# Collect network interface information
|
|
|
|
local primary_iface=$(detect_primary_interface)
|
|
local current_ip=$(get_current_ip)
|
|
local gateway=$(ip route show default 2>/dev/null | awk '/^default/ {print $3; exit}')
|
|
|
|
cat <<EOF
|
|
network:
|
|
primary_interface: "$primary_iface"
|
|
current_ip: "$current_ip"
|
|
gateway: "${gateway:-unknown}"
|
|
EOF
|
|
|
|
# MAC address
|
|
if [ "$primary_iface" != "unknown" ]; then
|
|
local mac=$(ip link show "$primary_iface" 2>/dev/null | grep "link/ether" | awk '{print $2}')
|
|
cat <<EOF
|
|
mac_address: "${mac:-unknown}"
|
|
EOF
|
|
fi
|
|
|
|
# Link speed if available
|
|
if [ "$primary_iface" != "unknown" ] && [ -f "/sys/class/net/$primary_iface/speed" ]; then
|
|
local speed=$(cat "/sys/class/net/$primary_iface/speed" 2>/dev/null || echo "unknown")
|
|
cat <<EOF
|
|
link_speed_mbps: $speed
|
|
EOF
|
|
fi
|
|
|
|
# All interfaces (brief)
|
|
local all_interfaces=$(ip -o link show | awk -F': ' '$2 != "lo" {printf " - \"%s\"\n", $2}')
|
|
if [ -n "$all_interfaces" ]; then
|
|
cat <<EOF
|
|
all_interfaces:
|
|
$all_interfaces
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
collect_gpu_facts() {
|
|
# Collect GPU information
|
|
|
|
local gpu_vendor=$(detect_gpu)
|
|
local gpu_model=$(get_gpu_model)
|
|
|
|
cat <<EOF
|
|
gpu:
|
|
vendor: "$gpu_vendor"
|
|
model: "$gpu_model"
|
|
EOF
|
|
|
|
# NVIDIA-specific details
|
|
if [ "$gpu_vendor" == "nvidia" ] && command -v nvidia-smi &>/dev/null; then
|
|
local nvidia_driver=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -n1)
|
|
local nvidia_cuda=$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -n1)
|
|
|
|
cat <<EOF
|
|
nvidia_driver: "${nvidia_driver:-unknown}"
|
|
cuda_capability: "${nvidia_cuda:-unknown}"
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
collect_os_facts() {
|
|
# Collect OS and kernel information
|
|
|
|
local os_family=$(detect_os_family)
|
|
local os_version=$(detect_os_version)
|
|
local kernel=$(uname -r)
|
|
local hostname=$(hostname)
|
|
local fqdn=$(hostname -f 2>/dev/null || echo "$hostname")
|
|
|
|
if [ -f /etc/os-release ]; then
|
|
. /etc/os-release
|
|
local os_name="$NAME"
|
|
local os_version_id="$VERSION_ID"
|
|
fi
|
|
|
|
cat <<EOF
|
|
os:
|
|
family: "$os_family"
|
|
name: "${os_name:-unknown}"
|
|
version: "$os_version"
|
|
version_id: "${os_version_id:-unknown}"
|
|
kernel: "$kernel"
|
|
hostname: "$hostname"
|
|
fqdn: "$fqdn"
|
|
EOF
|
|
}
|
|
|
|
collect_hardware_type_facts() {
|
|
# Collect deployment type and special hardware flags
|
|
|
|
local hw_type=$(detect_hardware_type)
|
|
local in_container=$(is_running_in_container && echo "true" || echo "false")
|
|
local is_vm=$(systemd-detect-virt --vm &>/dev/null && echo "true" || echo "false")
|
|
local virt_type=$(systemd-detect-virt 2>/dev/null || echo "none")
|
|
|
|
cat <<EOF
|
|
deployment:
|
|
hardware_type: "$hw_type"
|
|
is_virtual: $is_vm
|
|
virtualization: "$virt_type"
|
|
in_container: $in_container
|
|
EOF
|
|
|
|
# Proxmox-specific
|
|
if [ "$hw_type" == "proxmox" ] && command -v pveversion &>/dev/null; then
|
|
local pve_version=$(pveversion | head -n1 | awk '{print $2}')
|
|
cat <<EOF
|
|
proxmox_version: "$pve_version"
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
collect_timestamp_facts() {
|
|
# Collect collection timestamp
|
|
|
|
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
local date_collected=$(date +"%Y-%m-%d")
|
|
local epoch=$(date +%s)
|
|
|
|
cat <<EOF
|
|
metadata:
|
|
collected_at: "$timestamp"
|
|
date: "$date_collected"
|
|
epoch: $epoch
|
|
collector: "bootstrap.sh/fingerprint"
|
|
EOF
|
|
}
|
|
|
|
# --- STANDARDS VALIDATION ---
|
|
|
|
validate_against_standards() {
|
|
# Check hardware against environment-constraints.md requirements
|
|
# Returns validation status
|
|
|
|
local hw_type=$(detect_hardware_type)
|
|
local mem_gb=$(grep MemTotal /proc/meminfo | awk '{print int($2/1024/1024)}')
|
|
local cpu_cores=$(nproc)
|
|
local disk_gb=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
|
|
|
local violations=0
|
|
|
|
echo " standards_validation:" >&2
|
|
|
|
# Memory requirements
|
|
case "$hw_type" in
|
|
proxmox)
|
|
if [ "$mem_gb" -lt 8 ]; then
|
|
echo " - \"WARNING: Proxmox host has ${mem_gb}GB RAM (minimum 8GB recommended)\"" >&2
|
|
((violations++))
|
|
fi
|
|
;;
|
|
docker-vm|physical-docker)
|
|
if [ "$mem_gb" -lt 4 ]; then
|
|
echo " - \"WARNING: Docker host has ${mem_gb}GB RAM (minimum 4GB recommended)\"" >&2
|
|
((violations++))
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# Disk space
|
|
if [ "$disk_gb" -lt 20 ]; then
|
|
echo " - \"WARNING: Low disk space (${disk_gb}GB) - minimum 20GB recommended\"" >&2
|
|
((violations++))
|
|
fi
|
|
|
|
# CPU cores
|
|
if [ "$cpu_cores" -lt 2 ]; then
|
|
echo " - \"WARNING: Single CPU core detected - minimum 2 recommended\"" >&2
|
|
((violations++))
|
|
fi
|
|
|
|
if [ $violations -eq 0 ]; then
|
|
echo " - \"OK: Hardware meets minimum standards\"" >&2
|
|
fi
|
|
|
|
return $violations
|
|
}
|
|
|
|
# --- STRUCTURED OUTPUT ---
|
|
|
|
generate_hardware_facts_yaml() {
|
|
# Generate complete hardware facts in YAML format
|
|
# Compatible with gather_hardware_facts.yml output
|
|
|
|
local hostname=$(hostname)
|
|
|
|
cat <<EOF
|
|
---
|
|
# Hardware Facts Collection
|
|
# Generated by: bootstrap.sh fingerprint library
|
|
# Host: $hostname
|
|
# $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
|
|
|
$hostname:
|
|
$(collect_os_facts)
|
|
$(collect_cpu_facts)
|
|
$(collect_memory_facts)
|
|
$(collect_disk_facts)
|
|
$(collect_network_facts)
|
|
$(collect_gpu_facts)
|
|
$(collect_hardware_type_facts)
|
|
$(collect_timestamp_facts)
|
|
EOF
|
|
}
|
|
|
|
save_hardware_facts() {
|
|
# Save hardware facts to output file
|
|
# Args: $1 = output directory (default: ../ansible/archive/outputs)
|
|
|
|
local output_dir="${1:-../ansible/archive/outputs}"
|
|
local hostname=$(hostname)
|
|
local timestamp=$(date +%Y%m%dT%H%M%S)
|
|
local output_file="${output_dir}/hardware-facts-${hostname}-${timestamp}.yml"
|
|
|
|
# Ensure output directory exists
|
|
mkdir -p "$output_dir"
|
|
|
|
# Generate and save
|
|
generate_hardware_facts_yaml > "$output_file"
|
|
|
|
echo "$output_file"
|
|
}
|
|
|
|
# --- ANSIBLE INVENTORY GENERATION ---
|
|
|
|
generate_ansible_inventory_snippet() {
|
|
# Generate Ansible inventory entry for this host
|
|
# Can be appended to discovered-hosts.yml
|
|
|
|
local hostname=$(hostname)
|
|
local current_ip=$(get_current_ip)
|
|
local hw_type=$(detect_hardware_type)
|
|
local os_family=$(detect_os_family)
|
|
|
|
# Determine inventory group
|
|
local inventory_group="docker_hosts"
|
|
case "$hw_type" in
|
|
proxmox)
|
|
inventory_group="proxmox_cluster"
|
|
;;
|
|
docker-vm)
|
|
inventory_group="swarm_managers" # Or swarm_workers, requires manual classification
|
|
;;
|
|
pi)
|
|
inventory_group="control_nodes"
|
|
;;
|
|
ai-workstation)
|
|
inventory_group="ai_nodes"
|
|
;;
|
|
esac
|
|
|
|
cat <<EOF
|
|
# Auto-discovered: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
|
|
[$inventory_group]
|
|
$hostname ansible_host=$current_ip ansible_user=chester
|
|
|
|
# Hardware details:
|
|
# - Type: $hw_type
|
|
# - OS: $os_family
|
|
# - IP: $current_ip
|
|
# Review and merge into inventory/hosts.ini after validation
|
|
EOF
|
|
}
|
|
|
|
append_to_discovered_inventory() {
|
|
# Append this host to discovered-hosts.yml
|
|
# Args: $1 = inventory file (default: ../ansible/archive/inventory/discovered-hosts.yml)
|
|
|
|
local inventory_file="${1:-../ansible/archive/inventory/discovered-hosts.yml}"
|
|
local hostname=$(hostname)
|
|
|
|
# Create file if doesn't exist
|
|
if [ ! -f "$inventory_file" ]; then
|
|
cat > "$inventory_file" <<EOF
|
|
# Auto-Discovery Inventory
|
|
# Generated by bootstrap.sh - DO NOT MANUALLY EDIT
|
|
# Merge entries into hosts.ini after validation
|
|
|
|
EOF
|
|
fi
|
|
|
|
# Check if host already exists
|
|
if grep -q "^${hostname} " "$inventory_file" 2>/dev/null; then
|
|
echo "Host $hostname already in discovered inventory, skipping" >&2
|
|
return 0
|
|
fi
|
|
|
|
# Append
|
|
generate_ansible_inventory_snippet >> "$inventory_file"
|
|
echo "" >> "$inventory_file"
|
|
|
|
echo "$inventory_file"
|
|
}
|
|
|
|
# --- OUTPUT FORMAT OPTIONS ---
|
|
|
|
generate_json_output() {
|
|
# Generate JSON format instead of YAML (for monitoring integration)
|
|
# Converts YAML facts to JSON
|
|
|
|
local hostname=$(hostname)
|
|
local current_ip=$(get_current_ip)
|
|
local hw_type=$(detect_hardware_type)
|
|
local os_family=$(detect_os_family)
|
|
local cpu_cores=$(nproc)
|
|
local mem_gb=$(grep MemTotal /proc/meminfo | awk '{print int($2/1024/1024)}')
|
|
local disk_gb=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
|
|
|
cat <<EOF
|
|
{
|
|
"hostname": "$hostname",
|
|
"ip": "$current_ip",
|
|
"hardware_type": "$hw_type",
|
|
"os_family": "$os_family",
|
|
"cpu_cores": $cpu_cores,
|
|
"memory_gb": $mem_gb,
|
|
"disk_gb": $disk_gb,
|
|
"collected_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# --- SUMMARY DISPLAY ---
|
|
|
|
print_hardware_summary() {
|
|
# Print human-readable hardware summary
|
|
|
|
local hostname=$(hostname)
|
|
local hw_type=$(detect_hardware_type)
|
|
local os=$(detect_os_family) $(detect_os_version)
|
|
local cpu_model=$(grep "model name" /proc/cpuinfo | head -n1 | cut -d: -f2 | xargs)
|
|
local cpu_cores=$(nproc)
|
|
local mem_gb=$(grep MemTotal /proc/meminfo | awk '{print int($2/1024/1024)}')
|
|
local disk_gb=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//')
|
|
local gpu=$(detect_gpu)
|
|
local ip=$(get_current_ip)
|
|
|
|
cat >&2 <<EOF
|
|
=== Hardware Fingerprint Summary ===
|
|
Hostname: $hostname
|
|
IP Address: $ip
|
|
Type: $hw_type
|
|
OS: $os
|
|
CPU: $cpu_model ($cpu_cores cores)
|
|
Memory: ${mem_gb}GB
|
|
Storage: ${disk_gb}GB
|
|
GPU: $gpu
|
|
====================================
|
|
EOF
|
|
}
|
|
|
|
# Export functions
|
|
export -f collect_cpu_facts
|
|
export -f collect_memory_facts
|
|
export -f collect_disk_facts
|
|
export -f collect_network_facts
|
|
export -f collect_gpu_facts
|
|
export -f collect_os_facts
|
|
export -f collect_hardware_type_facts
|
|
export -f collect_timestamp_facts
|
|
export -f validate_against_standards
|
|
export -f generate_hardware_facts_yaml
|
|
export -f save_hardware_facts
|
|
export -f generate_ansible_inventory_snippet
|
|
export -f append_to_discovered_inventory
|
|
export -f generate_json_output
|
|
export -f print_hardware_summary
|