Compare commits

...

3 Commits

4 changed files with 255 additions and 4 deletions

View File

@@ -15,8 +15,7 @@
validate: /usr/sbin/visudo --check --file=%s
- name: Deploy per-user sudoers rules
# Jinja truthiness: bool true / a rule string => deploy; false / '' / unset => skip.
when: item.value.sudo | default(false)
when: (item.value.sudo | default(false)) not in [false, '', none]
vars:
configuration_sudoers_rule: >-
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}

View File

@@ -26,17 +26,22 @@
- name: Create user accounts
vars:
configuration_user_group: "{{ _configuration_platform.user_group }}"
# plaintext is hashed; a pre-computed crypt hash ($6$/$y$/...) passes through.
configuration_user_pw: >-
{{ item.value.password if (item.value.password | string)[:1] == '$'
else item.value.password | password_hash('sha512') }}
configuration_user_groups: >-
{{ item.value.groups | default(
[_configuration_platform.user_group]
if (item.value.sudo | default(false)) not in [false, '', none]
else []) }}
configuration_useradd_cmd: >-
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group
--uid {{ 1000 + _idx }}
--groups {{ configuration_user_group }} {{ item.key }}
{{ ('--groups ' ~ (configuration_user_groups | join(','))) if (configuration_user_groups | length) > 0 else '' }}
{{ ('--password ' ~ configuration_user_pw) if (item.value.password | default('') | string | length > 0) else '' }}
--shell {{ item.value.shell | default('/bin/bash') }}
{{ item.key }}
ansible.builtin.command: "{{ configuration_useradd_cmd }}"
loop: "{{ system_cfg.users | dict2items }}"
loop_control:

View File

@@ -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 "<storage>:iso/<file>.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 }}"

View File

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