diff --git a/roles/system_check/tasks/_placement.yml b/roles/system_check/tasks/_placement.yml new file mode 100644 index 0000000..aaf43b7 --- /dev/null +++ b/roles/system_check/tasks/_placement.yml @@ -0,0 +1,243 @@ +--- +- name: Collect the boot media this run attaches + ansible.builtin.set_fact: + system_check_media: >- + {{ [{'name': 'boot_iso', 'value': boot_iso}] + + ([{'name': 'rhel_iso', 'value': rhel_iso}] + if (rhel_iso | default('') | string | length > 0) else []) }} + +- name: Verify vCenter boot media and placement target + when: hypervisor_type == "vmware" + delegate_to: localhost + become: false + module_defaults: + group/community.vmware.vmware: + hostname: "{{ hypervisor_cfg.url }}" + username: "{{ hypervisor_cfg.username }}" + password: "{{ hypervisor_cfg.password }}" + validate_certs: "{{ hypervisor_cfg.certs | bool }}" + block: + - name: Assert the boot media is a datastore path + ansible.builtin.assert: + that: + - item.value is match('^\\[[^\\]]+\\]\\s*\\S') + fail_msg: | + {{ item.name }} is "{{ item.value }}", which is not a vSphere datastore path. + Set {{ item.name }} to the form "[datastore] folder/file.iso", + for example "[ssdmsa_lun1] ISO/SGBoot-2026.06.09-x86_64.iso". + quiet: true + loop: "{{ system_check_media }}" + loop_control: + label: "{{ item.name }}" + + - name: Resolve the datastores this run depends on + ansible.builtin.set_fact: + system_check_datastores_needed: >- + {%- set ns = namespace(refs=[{'source': 'hypervisor.storage', + 'datastore': hypervisor_cfg.storage, 'path': ''}]) -%} + {%- for media in system_check_media -%} + {%- set ns.refs = ns.refs + [{ + 'source': media.name, + 'datastore': media.value | regex_replace('^\\[([^\\]]+)\\].*$', '\\1'), + 'path': media.value | regex_replace('^\\[[^\\]]+\\]\\s*', '') + }] -%} + {%- endfor -%} + {{ ns.refs }} + + - name: Query the ESXi placement host + when: hypervisor_cfg.node | default('') | length > 0 + community.vmware.vmware_host_facts: + esxi_hostname: "{{ hypervisor_cfg.node }}" + register: system_check_esxi + delegate_facts: true + failed_when: false + changed_when: false + no_log: true + + - name: Assert the ESXi placement host is readable + when: hypervisor_cfg.node | default('') | length > 0 + ansible.builtin.assert: + that: + - system_check_esxi.ansible_facts is defined + fail_msg: | + ESXi host {{ hypervisor_cfg.node }} (hypervisor.node) could not be read from vCenter {{ hypervisor_cfg.url }}. + {{ system_check_esxi.msg | default('Unknown error') }} + Set hypervisor.node to an ESXi host in datacenter {{ hypervisor_cfg.datacenter }}, + or clear hypervisor.node and set hypervisor.cluster instead. + quiet: true + + - name: Assert the ESXi placement host can take a new VM + when: hypervisor_cfg.node | default('') | length > 0 + ansible.builtin.assert: + that: + - system_check_esxi.ansible_facts.ansible_host_connection_state == "connected" + - not (system_check_esxi.ansible_facts.ansible_in_maintenance_mode | bool) + fail_msg: | + ESXi host {{ hypervisor_cfg.node }} (hypervisor.node) cannot take a new VM: + connection state is {{ system_check_esxi.ansible_facts.ansible_host_connection_state }}, + maintenance mode is {{ system_check_esxi.ansible_facts.ansible_in_maintenance_mode }}. + Wait for the host to come back, or set hypervisor.node to another host. + quiet: true + + - name: Query the datastores of the placement cluster + when: hypervisor_cfg.node | default('') | length == 0 + community.vmware.vmware_datastore_info: + cluster: "{{ hypervisor_cfg.cluster }}" + register: system_check_cluster + failed_when: false + changed_when: false + no_log: true + + - name: Assert the placement cluster is readable + when: hypervisor_cfg.node | default('') | length == 0 + ansible.builtin.assert: + that: + - system_check_cluster.datastores is defined + fail_msg: | + Cluster {{ hypervisor_cfg.cluster }} (hypervisor.cluster) could not be read from vCenter {{ hypervisor_cfg.url }}. + {{ system_check_cluster.msg | default('Unknown error') }} + Set hypervisor.cluster to a cluster in datacenter {{ hypervisor_cfg.datacenter }}, + or clear hypervisor.cluster and pin hypervisor.node instead. + quiet: true + + - name: Resolve the datastores available at the placement target + ansible.builtin.set_fact: + system_check_target: >- + {{ ('ESXi host ' ~ hypervisor_cfg.node ~ ' (hypervisor.node)') + if (hypervisor_cfg.node | default('') | length > 0) + else ('cluster ' ~ hypervisor_cfg.cluster ~ ' (hypervisor.cluster)') }} + system_check_datastores_available: >- + {{ (system_check_esxi.ansible_facts.ansible_datastore | map(attribute='name') | list) + if (hypervisor_cfg.node | default('') | length > 0) + else (system_check_cluster.datastores | map(attribute='name') | list) }} + + - name: Assert every datastore this run needs is available at the placement target + ansible.builtin.assert: + that: + - item.datastore in system_check_datastores_available + fail_msg: | + Datastore {{ item.datastore }} (from {{ item.source }}) is not available on {{ system_check_target }}. + Datastores available there: {{ system_check_datastores_available | join(', ') }}. + Point {{ item.source }} at one of those datastores, or move placement to a host or cluster + that mounts {{ item.datastore }}. + quiet: true + loop: "{{ system_check_datastores_needed }}" + loop_control: + label: "{{ item.source }}: {{ item.datastore }}" + + - name: Query the boot media on its datastore + community.vmware.vsphere_file: + datacenter: "{{ hypervisor_cfg.datacenter }}" + datastore: "{{ item.datastore }}" + path: "{{ item.path }}" + state: file + register: system_check_media_files + failed_when: false + changed_when: false + loop: "{{ system_check_datastores_needed | rejectattr('source', 'equalto', 'hypervisor.storage') | list }}" + loop_control: + label: "{{ item.source }}" + + - name: Assert the boot media exists on its datastore + ansible.builtin.assert: + that: + - (item.status | default(0)) != 404 + fail_msg: | + {{ item.item.source }} points at "[{{ item.item.datastore }}] {{ item.item.path }}", + which does not exist on datastore {{ item.item.datastore }}. + Set {{ item.item.source }} to a file that exists there, normally the current SGBoot ISO. + quiet: true + loop: "{{ system_check_media_files.results }}" + loop_control: + label: "{{ item.item.source }}" + +- name: Verify Proxmox boot media + when: hypervisor_type == "proxmox" + delegate_to: localhost + become: false + module_defaults: + community.proxmox.proxmox_storage_contents_info: + api_host: "{{ hypervisor_cfg.url }}" + api_user: "{{ hypervisor_cfg.username }}" + api_password: "{{ hypervisor_cfg.password | default(omit, true) }}" + api_token_id: "{{ hypervisor_cfg.token_id | default(omit, true) }}" + api_token_secret: "{{ hypervisor_cfg.token_secret | default(omit, true) }}" + block: + - name: Assert the boot media is a storage volume id + ansible.builtin.assert: + that: + - item.value is match('^[^:]+:iso/\\S') + fail_msg: | + {{ item.name }} is "{{ item.value }}", which is not a Proxmox volume id. + Set {{ item.name }} to the form ":iso/.iso", + for example "local:iso/SGBoot-2026.06.09-x86_64.iso". + quiet: true + loop: "{{ system_check_media }}" + loop_control: + label: "{{ item.name }}" + + - name: Query the ISO volumes on the Proxmox node + community.proxmox.proxmox_storage_contents_info: + node: "{{ hypervisor_cfg.node }}" + storage: "{{ item.value | regex_replace('^([^:]+):.*$', '\\1') }}" + content: iso + register: system_check_proxmox_media + failed_when: false + changed_when: false + loop: "{{ system_check_media }}" + loop_control: + label: "{{ item.name }}" + + - name: Assert the boot media storage serves ISOs on the Proxmox node + ansible.builtin.assert: + that: + - item.proxmox_storage_content is defined + fail_msg: | + Storage {{ item.item.value | regex_replace('^([^:]+):.*$', '\\1') }} (from {{ item.item.name }}) + does not serve ISO content on Proxmox node {{ hypervisor_cfg.node }}. + {{ item.msg | default('Unknown error') }} + Point {{ item.item.name }} at a storage that is enabled for ISO images on that node, + or change hypervisor.node. + quiet: true + loop: "{{ system_check_proxmox_media.results }}" + loop_control: + label: "{{ item.item.name }}" + + - name: Assert the boot media exists on the Proxmox node + ansible.builtin.assert: + that: + - item.item.value in (item.proxmox_storage_content | map(attribute='volid') | list) + fail_msg: | + {{ item.item.name }} points at "{{ item.item.value }}", which does not exist on Proxmox node + {{ hypervisor_cfg.node }}. + ISO volumes on that storage: {{ item.proxmox_storage_content | map(attribute='volid') | join(', ') }}. + Set {{ item.item.name }} to one of those volumes. + quiet: true + loop: "{{ system_check_proxmox_media.results }}" + loop_control: + label: "{{ item.item.name }}" + +- name: Verify libvirt boot media + when: hypervisor_type == "libvirt" + delegate_to: localhost + become: false + block: + - name: Stat the libvirt boot media + ansible.builtin.stat: + path: "{{ item.value }}" + register: system_check_libvirt_media + loop: "{{ system_check_media }}" + loop_control: + label: "{{ item.name }}" + + - name: Assert the libvirt boot media exists + ansible.builtin.assert: + that: + - item.stat.exists + fail_msg: | + {{ item.item.name }} points at "{{ item.item.value }}", which does not exist on the libvirt host. + Set {{ item.item.name }} to the path of an ISO file that exists there. + quiet: true + loop: "{{ system_check_libvirt_media.results }}" + loop_control: + label: "{{ item.item.name }}" diff --git a/roles/system_check/tasks/main.yml b/roles/system_check/tasks/main.yml index 303eb75..95a7b6a 100644 --- a/roles/system_check/tasks/main.yml +++ b/roles/system_check/tasks/main.yml @@ -132,3 +132,7 @@ To avoid data loss, the playbook will not overwrite or delete existing VMs. Please choose a different hostname or remove the existing VM manually before proceeding. quiet: true + +- name: Verify boot media and placement target + when: system_cfg.type == "virtual" + ansible.builtin.include_tasks: _placement.yml