homelab/ansible/archive/playbooks/preflight/gather_hardware_facts.yml

321 lines
11 KiB
YAML

---
# playbooks/preflight/gather_hardware_facts.yml
#
# Purpose:
# Gather comprehensive hardware specifications from Proxmox hosts for
# capacity planning and topology analysis (e.g., Docker Swarm 3-node cluster)
#
# Usage:
# ansible-playbook -i inventory/hosts.ini playbooks/preflight/gather_hardware_facts.yml \
# -e "target_hosts=proxmox_cluster" \
# -e "output_file=hardware_comparison_$(date +%Y%m%d).yml"
#
# Modules Explained:
# - ansible.builtin.gather_facts: Collects system facts (CPU, RAM, network, OS)
# WHY: Native, zero-install, idempotent. Beats shell commands because it's structured.
# - ansible.builtin.setup: Explicit fact-gathering (redundant but explicit docs)
# - ansible.builtin.command: For Proxmox-specific queries (pvesh, pveversion)
# WHY: Cleaner than shell for simple commands; no shell interpretation risk
# - ansible.builtin.copy: Save structured YAML report locally
# WHY: Idempotent, checksummed, handles permissions correctly
#
# Idempotency:
# All tasks are read-only (gather facts, query status). No state changes.
# Safe to run multiple times without side effects.
#
# Safety Notes:
# - Does NOT modify Proxmox configuration or cluster state
# - Requires root SSH access (standard Proxmox assumption)
# - Network scanning is passive (no stress-testing)
- name: "Gather Hardware Facts from Proxmox Hosts"
hosts: "{{ target_hosts | default('proxmox_cluster') }}"
gather_facts: true
become: true
vars:
output_dir: "{{ playbook_dir }}/../../outputs"
output_file: "{{ output_dir }}/hardware_facts_{{ ansible_date_time.iso8601_basic_short }}.yml"
pre_tasks:
- name: "Validate target hosts are Proxmox nodes"
ansible.builtin.assert:
that:
- inventory_hostname.startswith('pve')
fail_msg: "This playbook is for Proxmox nodes (pve*). Target: {{ inventory_hostname }}"
tags:
- validate
- name: "Ensure output directory exists"
ansible.builtin.file:
path: "{{ output_dir }}"
state: directory
mode: '0755'
delegate_to: localhost
run_once: true
tags:
- setup
tasks:
# -------------------------------------------------------------------------
# SECTION 1: SYSTEM & OS FACTS
# -------------------------------------------------------------------------
- name: "1.1 Gather all facts (native Ansible)"
ansible.builtin.setup:
tags:
- facts
- always
- name: "1.2 Extract CPU model and frequencies"
ansible.builtin.set_fact:
hw_cpu_model: "{{ ansible_processor | first | default('unknown') }}"
hw_cpu_cores: "{{ ansible_processor_vcpus | default('unknown') }}"
hw_cpu_cores_per_socket: "{{ ansible_processor_cores | default('unknown') }}"
hw_cpu_count: "{{ ansible_processor_count | default(1) }}"
tags:
- facts
- cpu
- name: "1.3 Query CPU frequency (max freq)"
ansible.builtin.shell: "grep 'cpu MHz' /proc/cpuinfo | head -1"
register: _cpu_freq_output
changed_when: false
failed_when: false
tags:
- facts
- cpu
- name: "1.4 Parse CPU frequency"
ansible.builtin.set_fact:
hw_cpu_freq_mhz: "{{ (_cpu_freq_output.stdout | regex_search(':\\s+([0-9.]+)') | regex_replace('[^0-9.]', '')).split('.')[0] | float if _cpu_freq_output.stdout else 'unknown' }}"
tags:
- facts
- cpu
- name: "1.5 Extract RAM information"
ansible.builtin.set_fact:
hw_ram_total_mb: "{{ ansible_memtotal_mb | default('unknown') }}"
hw_ram_free_mb: "{{ ansible_memfree_mb | default('unknown') }}"
tags:
- facts
- ram
- name: "1.6 Extract OS and kernel"
ansible.builtin.set_fact:
hw_os_family: "{{ ansible_os_family }}"
hw_os_distro: "{{ ansible_distribution }}"
hw_os_release: "{{ ansible_distribution_release }}"
hw_kernel: "{{ ansible_kernel }}"
hw_uptime_seconds: "{{ ansible_uptime_seconds }}"
tags:
- facts
- os
# -------------------------------------------------------------------------
# SECTION 2: STORAGE FACTS
# -------------------------------------------------------------------------
- name: "2.1 Gather disk information"
ansible.builtin.set_fact:
hw_disks: "{{ ansible_devices | default({}) | dict2items | map(attribute='key') | list }}"
tags:
- facts
- storage
- name: "2.2 Gather mount points and usage"
ansible.builtin.shell: |
df -h | tail -n +2 | awk '{print $1 " (" $4 " available of " $2 ")"}'
register: _mount_output
changed_when: false
tags:
- facts
- storage
- name: "2.3 Parse mount data"
ansible.builtin.set_fact:
hw_mounts: "{{ _mount_output.stdout_lines | default([]) }}"
tags:
- facts
- storage
# -------------------------------------------------------------------------
# SECTION 3: NETWORK FACTS
# -------------------------------------------------------------------------
- name: "3.1 Gather network interface details"
ansible.builtin.set_fact:
hw_network_interfaces: "{{ ansible_interfaces | default([]) }}"
tags:
- facts
- network
# -----------------------------------------------------------------------
# SECTION 4: PROXMOX-SPECIFIC FACTS
# -----------------------------------------------------------------------
- name: "4.1 Query Proxmox version"
ansible.builtin.command: "pveversion"
register: _pveversion_output
changed_when: false
tags:
- facts
- proxmox
- name: "4.2 Parse Proxmox version"
ansible.builtin.set_fact:
hw_proxmox_version: "{{ _pveversion_output.stdout | regex_search('proxmox-ve\\s+([0-9.]+)') | regex_replace('[^0-9.]', '') }}"
hw_pveversion_full: "{{ _pveversion_output.stdout }}"
tags:
- facts
- proxmox
- name: "4.3 Query Proxmox cluster status"
ansible.builtin.command: "pvesh get /cluster/resources --output-format json"
register: _cluster_resources
changed_when: false
failed_when: false
tags:
- facts
- proxmox
- name: "4.4 Count VMs and containers on this host"
ansible.builtin.set_fact:
hw_vm_count: "{{ (_cluster_resources.stdout | from_json | selectattr('node', 'equalto', inventory_hostname) | selectattr('type', 'match', 'qemu|lxc') | list | length) if _cluster_resources.rc == 0 else 'unknown' }}"
tags:
- facts
- proxmox
- name: "4.5 Query Proxmox cluster info"
ansible.builtin.command: "pvesh get /cluster/status --output-format json"
register: _cluster_status
changed_when: false
failed_when: false
tags:
- facts
- proxmox
- name: "4.6 Parse cluster membership"
ansible.builtin.set_fact:
hw_cluster_name: "{{ (_cluster_status.stdout | from_json | first).cluster | default('not-clustered') if _cluster_status.rc == 0 and (_cluster_status.stdout | from_json | length) > 0 else 'not-clustered' }}"
hw_cluster_nodes: "{{ (_cluster_status.stdout | from_json | map(attribute='name') | list) if _cluster_status.rc == 0 else [] }}"
tags:
- facts
- proxmox
- name: "4.7 Check if node is clustered"
ansible.builtin.set_fact:
hw_is_clustered: "{{ inventory_hostname in hw_cluster_nodes }}"
tags:
- facts
- proxmox
# -----------------------------------------------------------------------
# SECTION 5: SYSTEM LOAD & CAPACITY
# -----------------------------------------------------------------------
- name: "5.1 Gather system load"
ansible.builtin.set_fact:
hw_load_1min: "{{ ansible_load | default([0, 0, 0]) | first }}"
hw_load_5min: "{{ (ansible_load | default([0, 0, 0]))[1] }}"
hw_load_15min: "{{ (ansible_load | default([0, 0, 0]))[2] }}"
tags:
- facts
- load
- name: "5.2 Calculate CPU usage percentage"
ansible.builtin.set_fact:
hw_cpu_load_percent: "{{ ((hw_load_1min | float / hw_cpu_cores | int) * 100) | int if hw_load_1min != 0 else 0 }}"
tags:
- facts
- load
post_tasks:
- name: "Build hardware summary fact"
ansible.builtin.set_fact:
hardware_summary:
hostname: "{{ inventory_hostname }}"
ip_address: "{{ ansible_default_ipv4.address }}"
fqdn: "{{ ansible_fqdn }}"
timestamp: "{{ ansible_date_time.iso8601 }}"
system:
os: "{{ hw_os_distro }} {{ hw_os_release }}"
kernel: "{{ hw_kernel }}"
uptime_days: "{{ (hw_uptime_seconds / 86400) | int }}"
cpu:
model: "{{ hw_cpu_model }}"
sockets: "{{ hw_cpu_count }}"
cores_per_socket: "{{ hw_cpu_cores_per_socket }}"
total_cores: "{{ hw_cpu_cores }}"
max_frequency_mhz: "{{ hw_cpu_freq_mhz | int if hw_cpu_freq_mhz != 'unknown' else 'unknown' }}"
current_1min_load: "{{ hw_load_1min }}"
cpu_load_percent: "{{ hw_cpu_load_percent }}%"
memory:
total_mb: "{{ hw_ram_total_mb }}"
total_gb: "{{ (hw_ram_total_mb | int / 1024) | int if hw_ram_total_mb != 'unknown' else 'unknown' }}"
free_mb: "{{ hw_ram_free_mb }}"
free_gb: "{{ (hw_ram_free_mb | int / 1024) | int if hw_ram_free_mb != 'unknown' else 'unknown' }}"
storage:
disks_detected: "{{ hw_disks | length }}"
disk_list: "{{ hw_disks }}"
mounts_summary: "{{ hw_mounts }}"
network:
interfaces_count: "{{ hw_network_interfaces | length }}"
interface_list: "{{ hw_network_interfaces }}"
proxmox:
version: "{{ hw_proxmox_version }}"
version_full: "{{ hw_pveversion_full }}"
cluster_name: "{{ hw_cluster_name | default('not-clustered') }}"
is_clustered: "{{ hw_is_clustered }}"
cluster_members: "{{ hw_cluster_nodes | default([]) }}"
vms_and_containers: "{{ hw_vm_count }}"
tags:
- summary
- always
- name: "Display hardware summary"
ansible.builtin.debug:
msg: "{{ hardware_summary }}"
tags:
- summary
- always
- name: "Collect all hardware facts for report"
ansible.builtin.set_fact:
all_hardware_facts: "{{ all_hardware_facts | default({}) | combine({inventory_hostname: hardware_summary}) }}"
tags:
- report
- name: "Write hardware comparison report"
ansible.builtin.copy:
content: |
---
# Hardware Facts Report
# Generated: {{ ansible_date_time.iso8601 }}
# Hosts Analyzed: {{ groups[target_hosts | default('proxmox_cluster')] | length }}
#
# Usage:
# This report compares hardware specifications for Docker Swarm topology planning.
# See README in documentation/architecture/ for capacity analysis.
{{ all_hardware_facts | to_nice_yaml }}
dest: "{{ output_file }}"
mode: '0644'
delegate_to: localhost
run_once: true
tags:
- report
- name: "Report output file location"
ansible.builtin.debug:
msg: "✓ Hardware facts saved to: {{ output_file }}"
delegate_to: localhost
run_once: true
tags:
- report