homelab/scripts/lib/fingerprint.sh
nathan e16f98a183 feat(bootstrap)!: introduce unified bootstrap system with modular libraries
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
2026-04-12 22:48:19 -04:00

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