--- # 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