Compare commits
68 Commits
master
...
4fff9f8d80
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fff9f8d80 | |||
| 7f12a0f3d8 | |||
| ceb2237bbb | |||
| 477c8379c4 | |||
| 579c499c02 | |||
| 89e366d0f0 | |||
| 6fe843355e | |||
| 441876fab9 | |||
| 00acd4d200 | |||
| d922efd2e4 | |||
| 939c5c741f | |||
| 2c35409519 | |||
| d2a19cfd5c | |||
| 44f5adc682 | |||
| 0185797af9 | |||
| e0ecf628cd | |||
| 37df881daa | |||
| 55b21eae5d | |||
| b1e938b7f0 | |||
| c843f5289b | |||
| 9757ed3785 | |||
| 876e90ce2b | |||
| 7c44cb1ff0 | |||
| 5d0630a386 | |||
| 3eaf918a53 | |||
| 382e82ff85 | |||
| db7dc53bd7 | |||
| 7d45f25a7e | |||
| 3880b8f41e | |||
| dc3c4a901f | |||
| 6bfaa0aa2b | |||
| 5dc0424acb | |||
| c12332a853 | |||
| 5ace4577a2 | |||
| c0ff01ef40 | |||
| 57417514e3 | |||
| 0928588c1f | |||
| 6d622f2db4 | |||
| b11d65a6f3 | |||
| 3623fc292c | |||
| dfca7ec94b | |||
| e8be84bf49 | |||
| 322cc0b1ce | |||
| 4b38754f8b | |||
| a6bc7ffe04 | |||
| c529e71ebc | |||
| cb46de2b6d | |||
| 9169117b25 | |||
| 6c94c519fb | |||
| efd96a42b8 | |||
| 68661c3cca | |||
| 1db20c7ac0 | |||
| 7b155b427b | |||
| ca8721e98f | |||
| cdb2559d8f | |||
| 443f6623df | |||
| 6cf418fe00 | |||
| 47ec5fe621 | |||
| 240f945cce | |||
| 663a04556f | |||
| 6febd1acf1 | |||
| 008187860c | |||
| cd1be6b5e1 | |||
| 15be6149fd | |||
| ca29ad200d | |||
| 8079099cee | |||
| 9e79185b07 | |||
| b88bf2860f |
@@ -183,7 +183,7 @@ Top-level variables outside `system`/`hypervisor`.
|
||||
|
||||
#### `system.content`
|
||||
|
||||
Uniform package content source, family-resolved. `source: ''` defaults to `dvd` on `rhel` and to `mirror` everywhere else, AlmaLinux, Rocky and Fedora included: they have public mirrors, RHEL does not. Satellite values come from inventory/vault only, never committed code.
|
||||
Uniform package content source, family-resolved. `source: ''` defaults to `dvd` on EL and `mirror` on Debian/Ubuntu/Arch. Satellite values come from inventory/vault only, never committed code.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| -------------------------- | ------ | -------------- | ----------------------------------------------------------------- |
|
||||
@@ -538,8 +538,6 @@ When `hypervisor.type: vmware` uses the `vmware_tools` connection:
|
||||
|
||||
`system.disks[0]` is the OS disk (no `mount.path`). Additional entries define data disks.
|
||||
|
||||
On VMware, `device` is resolved on the target from the SCSI address instead: entry N is the disk at target N on the first SCSI controller (N+1 from index 7 on, since unit 7 is reserved). The kernel's `sdX` order is not stable between the install environment and the installed system, so a letter-derived path can point at another disk.
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------- | ------ | ------------------------------------------------------ |
|
||||
| `size` | number | Disk size in GB (required for virtual) |
|
||||
|
||||
@@ -9,7 +9,7 @@ bootstrap_hardware_packages:
|
||||
amd: [amd-ucode]
|
||||
firmware_base: []
|
||||
firmware:
|
||||
intel: [linux-firmware-intel] # iwlwifi (WiFi) + i915/xe (GPU) + ibt (BT) firmware live here
|
||||
intel: [linux-firmware-other] # iwlwifi + i915 firmware live here
|
||||
amd: [linux-firmware-amdgpu]
|
||||
nvidia: [linux-firmware-nvidia]
|
||||
atheros: [linux-firmware-atheros]
|
||||
|
||||
@@ -206,7 +206,6 @@ bootstrap_debian:
|
||||
- openssh-server
|
||||
- python3
|
||||
- xfsprogs
|
||||
- xz-utils
|
||||
extra:
|
||||
- bat
|
||||
- curl
|
||||
@@ -259,7 +258,6 @@ bootstrap_ubuntu:
|
||||
- openssh-server
|
||||
- python3
|
||||
- xfsprogs
|
||||
- xz-utils
|
||||
extra:
|
||||
- bash-completion
|
||||
- bat
|
||||
|
||||
@@ -3,5 +3,3 @@
|
||||
cleanup_verify_boot: true
|
||||
cleanup_boot_timeout: 300
|
||||
cleanup_remove_on_failure: true
|
||||
cleanup_poweroff_retries: 24
|
||||
cleanup_poweroff_delay: 5
|
||||
|
||||
@@ -6,30 +6,7 @@
|
||||
module_defaults:
|
||||
community.proxmox.proxmox_disk: "{{ _proxmox_auth }}"
|
||||
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
|
||||
community.proxmox.proxmox_vm_info: "{{ _proxmox_auth_node }}"
|
||||
block:
|
||||
- name: Wait for the installer environment to power off
|
||||
community.proxmox.proxmox_vm_info:
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
register: cleanup_proxmox_power
|
||||
until: >-
|
||||
((cleanup_proxmox_power.proxmox_vms | default([]) | first | default({})).status
|
||||
| default('')) == 'stopped'
|
||||
retries: "{{ cleanup_poweroff_retries }}"
|
||||
delay: "{{ cleanup_poweroff_delay }}"
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Force the installer environment off when it did not shut down
|
||||
when: >-
|
||||
((cleanup_proxmox_power.proxmox_vms | default([]) | first | default({})).status
|
||||
| default('')) != 'stopped'
|
||||
community.proxmox.proxmox_kvm:
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
state: stopped
|
||||
force: true
|
||||
no_log: true
|
||||
|
||||
- name: Cleanup Setup Disks
|
||||
community.proxmox.proxmox_disk:
|
||||
name: "{{ hostname }}"
|
||||
@@ -44,8 +21,8 @@
|
||||
failed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Boot the installed OS
|
||||
- name: Start the VM
|
||||
community.proxmox.proxmox_kvm:
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
state: started
|
||||
state: restarted
|
||||
no_log: true
|
||||
|
||||
@@ -5,26 +5,9 @@
|
||||
become: false
|
||||
module_defaults:
|
||||
community.vmware.vmware_guest: "{{ _vmware_auth }}"
|
||||
community.vmware.vmware_guest_info: "{{ _vmware_auth }}"
|
||||
vmware.vmware.vm_powerstate: "{{ _vmware_auth }}"
|
||||
no_log: true
|
||||
block:
|
||||
- name: Wait for the installer environment to power off
|
||||
community.vmware.vmware_guest_info:
|
||||
name: "{{ hostname }}"
|
||||
register: cleanup_vmware_power
|
||||
until: (cleanup_vmware_power.instance.hw_power_status | default('') | lower) == 'poweredoff'
|
||||
retries: "{{ cleanup_poweroff_retries }}"
|
||||
delay: "{{ cleanup_poweroff_delay }}"
|
||||
failed_when: false
|
||||
|
||||
- name: Force the installer environment off when it did not shut down
|
||||
when: (cleanup_vmware_power.instance.hw_power_status | default('') | lower) != 'poweredoff'
|
||||
vmware.vmware.vm_powerstate:
|
||||
name: "{{ hostname }}"
|
||||
state: powered-off
|
||||
force: true
|
||||
|
||||
- name: Remove CD-ROM from VM in vCenter
|
||||
community.vmware.vmware_guest:
|
||||
name: "{{ hostname }}"
|
||||
@@ -58,7 +41,7 @@
|
||||
}}
|
||||
failed_when: false
|
||||
|
||||
- name: Boot the installed OS
|
||||
- name: Start VM in vCenter
|
||||
vmware.vmware.vm_powerstate:
|
||||
name: "{{ hostname }}"
|
||||
state: powered-on
|
||||
|
||||
@@ -18,24 +18,13 @@
|
||||
register: configuration_bootloader_result
|
||||
changed_when: configuration_bootloader_result.rc == 0
|
||||
|
||||
- name: Remove stale EFI boot entries for this OS
|
||||
# UEFI NVRAM entries survive a disk wipe (they live in firmware, not on disk).
|
||||
# A prior install leaves a dead entry, and a case-insensitive duplicate label
|
||||
# ("ArchLinux" vs "archlinux") would otherwise win the boot order - the firmware
|
||||
# then falls through to removable media. Delete every entry carrying our label,
|
||||
# then recreate exactly one clean entry.
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -o pipefail;
|
||||
efibootmgr | grep -iP '^Boot[0-9A-F]{4}\*?\s+{{ _efi_vendor }}\s*$'
|
||||
| grep -oP '^Boot\K[0-9A-F]{4}'
|
||||
| xargs -r -I{} efibootmgr -b {} -B
|
||||
executable: /bin/bash
|
||||
register: _efi_gc_result
|
||||
changed_when: _efi_gc_result.stdout | length > 0
|
||||
failed_when: false
|
||||
- name: Check existing EFI boot entries
|
||||
ansible.builtin.command: efibootmgr
|
||||
register: configuration_efi_entries
|
||||
changed_when: false
|
||||
|
||||
- name: Create EFI boot entry (efibootmgr -c prepends it to BootOrder)
|
||||
- name: Ensure EFI boot entry exists
|
||||
when: ('* ' + _efi_vendor) not in configuration_efi_entries.stdout
|
||||
ansible.builtin.command: >-
|
||||
efibootmgr -c
|
||||
-L '{{ _efi_vendor }}'
|
||||
@@ -45,16 +34,15 @@
|
||||
register: configuration_efi_entry_result
|
||||
changed_when: configuration_efi_entry_result.rc == 0
|
||||
|
||||
- name: Install removable EFI fallback loader
|
||||
# Firmware-agnostic fallback (\EFI\BOOT\BOOTX64.EFI): many consumer firmwares
|
||||
# ignore or reset custom NVRAM entries and boot only the removable path.
|
||||
when: _configuration_platform.grub_install
|
||||
ansible.builtin.command: >-
|
||||
{{ chroot_command }} /usr/bin/install -Dm644
|
||||
{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/{{ _efi_loader }}
|
||||
{{ partitioning_efi_mountpoint }}/EFI/BOOT/BOOTX64.EFI
|
||||
register: _efi_fallback_result
|
||||
changed_when: _efi_fallback_result.rc == 0
|
||||
- name: Set installed OS as first EFI boot entry
|
||||
ansible.builtin.shell:
|
||||
cmd: >-
|
||||
set -o pipefail &&
|
||||
efibootmgr | grep -i '{{ _efi_vendor }}' | grep -oP 'Boot\K[0-9A-F]+' | head -1
|
||||
| xargs -I{} efibootmgr -o {}
|
||||
executable: /bin/bash
|
||||
register: _efi_bootorder_result
|
||||
changed_when: _efi_bootorder_result.rc == 0
|
||||
|
||||
- name: Ensure lvm2 for non btrfs filesystems
|
||||
when: os == "archlinux" and system_cfg.filesystem != "btrfs"
|
||||
|
||||
@@ -12,21 +12,6 @@
|
||||
and 'No such file or directory' not in (_firewall_enable.stderr | default(''))
|
||||
and 'does not exist' not in (_firewall_enable.stderr | default(''))
|
||||
|
||||
# Distro base sets ship the daemon preset-enabled (RHEL @core pulls firewalld in),
|
||||
# so opting out has to disable the unit, not merely skip enabling it.
|
||||
- name: Disable the firewall daemon in the install chroot
|
||||
when:
|
||||
- firewall_phase == 'install'
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- not system_cfg.features.firewall.enabled | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl disable {{ system_cfg.features.firewall.backend }}"
|
||||
register: _firewall_disable
|
||||
changed_when: "'Removed' in (_firewall_disable.stderr | default(''))"
|
||||
failed_when: >-
|
||||
_firewall_disable.rc != 0
|
||||
and 'No such file or directory' not in (_firewall_disable.stderr | default(''))
|
||||
and 'does not exist' not in (_firewall_disable.stderr | default(''))
|
||||
|
||||
# ufw's CLI needs a running kernel and is a no-op in the chroot (leaves ENABLED=no),
|
||||
# so its activation and SSH rule are applied here, after reboot.
|
||||
- name: Allow SSH through ufw before enabling
|
||||
|
||||
@@ -43,14 +43,11 @@
|
||||
}}
|
||||
grub_root_flags: >-
|
||||
{{ ['rootflags=subvol=@'] if system_cfg.filesystem == 'btrfs' else [] }}
|
||||
grub_selinux_args: >-
|
||||
{{ [] if system_cfg.features.selinux.enabled | bool else ['selinux=0'] }}
|
||||
# String-concat (not list-concat like grub_kernel_cmdline_base below): ansible-lint's
|
||||
# jinja render trips on list+list when grub_lvm_args leads the expression here.
|
||||
grub_cmdline_linux_base: >-
|
||||
{{
|
||||
((grub_lvm_args | join(' ')) ~ ' ' ~ (grub_selinux_args | join(' ')) ~ ' '
|
||||
~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
|
||||
((grub_lvm_args | join(' ')) ~ ' ' ~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
|
||||
}}
|
||||
grub_kernel_cmdline_base: >-
|
||||
{{
|
||||
@@ -60,7 +57,6 @@
|
||||
+ ['ro']
|
||||
+ grub_lvm_args
|
||||
+ grub_root_flags
|
||||
+ grub_selinux_args
|
||||
+ (_hardware_profile_kernel_params | default([]))
|
||||
)
|
||||
| join(' ')
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
org_id: "{{ system_cfg.content.satellite.org }}"
|
||||
activationkey: "{{ system_cfg.content.satellite.activation_key }}"
|
||||
environment: "{{ system_cfg.content.satellite.environment | default(omit, true) }}"
|
||||
auto_attach: true
|
||||
force_register: true
|
||||
server_proxy_hostname: "{{ (system_cfg.content.proxy | urlsplit('hostname')) | default(omit, true) }}"
|
||||
server_proxy_port: "{{ (system_cfg.content.proxy | urlsplit('port')) | default(omit, true) }}"
|
||||
|
||||
@@ -22,12 +22,10 @@
|
||||
mode: "0644"
|
||||
|
||||
# Fedora: setfiles segfaults during bootstrap chroot relabeling, so SELinux
|
||||
# is left permissive and expected to relabel on first boot. Opting out writes
|
||||
# disabled here and selinux=0 on the kernel cmdline (grub.yml), the only form
|
||||
# EL9 honours.
|
||||
- name: Set the SELinux mode
|
||||
# is left permissive and expected to relabel on first boot.
|
||||
- name: Disable SELinux
|
||||
when: os == "fedora" or not system_cfg.features.selinux.enabled | bool
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/selinux/config
|
||||
regexp: ^SELINUX=
|
||||
line: "SELINUX={{ 'permissive' if system_cfg.features.selinux.enabled | bool else 'disabled' }}"
|
||||
line: SELINUX=permissive
|
||||
|
||||
@@ -54,18 +54,6 @@
|
||||
and 'No such file or directory' not in (configuration_enable_service_result.stderr | default(''))
|
||||
and 'does not exist' not in (configuration_enable_service_result.stderr | default(''))
|
||||
|
||||
- name: Check for the EL qemu-guest-agent RPC allow-list
|
||||
ansible.builtin.stat:
|
||||
path: /mnt/etc/sysconfig/qemu-ga
|
||||
register: configuration_qga_sysconfig
|
||||
|
||||
- name: Allow clone-stamping RPCs in the EL qemu-guest-agent allow-list
|
||||
when: configuration_qga_sysconfig.stat.exists
|
||||
ansible.builtin.replace:
|
||||
path: /mnt/etc/sysconfig/qemu-ga
|
||||
regexp: '^(FILTER_RPC_ARGS="--allow-rpcs=(?:(?!guest-exec)[^"])*)"'
|
||||
replace: '\1,guest-exec,guest-exec-status,guest-file-open,guest-file-close,guest-file-read,guest-file-write"'
|
||||
|
||||
- name: Enable display manager for selected desktop
|
||||
when:
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
validate: /usr/sbin/visudo --check --file=%s
|
||||
|
||||
- name: Deploy per-user sudoers rules
|
||||
when: (item.value.sudo | default(false)) not in [false, '', none]
|
||||
# Jinja truthiness: bool true / a rule string => deploy; false / '' / unset => skip.
|
||||
when: item.value.sudo | default(false)
|
||||
vars:
|
||||
configuration_sudoers_rule: >-
|
||||
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
|
||||
|
||||
@@ -26,22 +26,17 @@
|
||||
|
||||
- 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_groups | join(','))) if (configuration_user_groups | length) > 0 else '' }}
|
||||
--groups {{ configuration_user_group }} {{ item.key }}
|
||||
{{ ('--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:
|
||||
|
||||
@@ -73,25 +73,28 @@
|
||||
mode: "0755"
|
||||
|
||||
- name: Detect RHEL ISO device
|
||||
ansible.builtin.command: lsblk -rbno NAME,TYPE,SIZE
|
||||
ansible.builtin.command: lsblk -rno NAME,TYPE
|
||||
register: environment_lsblk_result
|
||||
changed_when: false
|
||||
|
||||
- name: Select RHEL ISO device
|
||||
vars:
|
||||
_roms: >-
|
||||
{%- set out = [] -%}
|
||||
{%- for line in environment_lsblk_result.stdout_lines -%}
|
||||
{%- set p = line.split() -%}
|
||||
{%- if (p | length) >= 3 and p[1] == 'rom' -%}
|
||||
{%- set _ = out.append({'name': p[0], 'size': p[2] | int}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
_rom_devices: >-
|
||||
{{
|
||||
environment_lsblk_result.stdout_lines
|
||||
| map('split', ' ')
|
||||
| selectattr('1', 'equalto', 'rom')
|
||||
| map('first')
|
||||
| map('regex_replace', '^', '/dev/')
|
||||
| list
|
||||
}}
|
||||
ansible.builtin.set_fact:
|
||||
environment_rhel_iso_device: >-
|
||||
{{ ('/dev/' ~ (_roms | sort(attribute='size') | last).name)
|
||||
if (_roms | length) > 0 else '/dev/sr1' }}
|
||||
{{
|
||||
_rom_devices[-1]
|
||||
if _rom_devices | length > 1
|
||||
else (_rom_devices[0] | default('/dev/sr1'))
|
||||
}}
|
||||
|
||||
- name: Mount RHEL ISO
|
||||
ansible.posix.mount:
|
||||
|
||||
@@ -78,8 +78,7 @@ system_defaults:
|
||||
timezone: "Europe/Vienna"
|
||||
locale: "en_US.UTF-8"
|
||||
keymap: "us"
|
||||
# source: dvd|mirror|satellite|none ('' -> family default: rhel=dvd, else mirror;
|
||||
# almalinux/rocky/fedora have public mirrors, rhel does not).
|
||||
# source: dvd|mirror|satellite|none ('' -> family default: EL=dvd, else mirror).
|
||||
# satellite values come from inventory/vault only, never committed code.
|
||||
content:
|
||||
source: ""
|
||||
|
||||
@@ -94,7 +94,6 @@
|
||||
|
||||
- name: Set install_drive from primary disk
|
||||
when:
|
||||
- install_drive is not defined
|
||||
- system_disks_cfg | length > 0
|
||||
- system_disks_cfg[0].device | string | length > 0
|
||||
ansible.builtin.set_fact:
|
||||
|
||||
@@ -35,12 +35,10 @@
|
||||
}, recursive=True)
|
||||
}}
|
||||
|
||||
# Keyed on os, not hostname: inventories set hostname themselves, and doing so
|
||||
# would otherwise skip enrichment and leave os undefined for validation.
|
||||
- name: Check if pre-computed system_cfg needs enrichment
|
||||
when: system_cfg is defined
|
||||
ansible.builtin.set_fact:
|
||||
_bootstrap_needs_enrichment: "{{ os is not defined }}"
|
||||
_bootstrap_needs_enrichment: "{{ hostname is not defined }}"
|
||||
|
||||
- name: Merge pre-computed system_cfg with bootstrap system_defaults
|
||||
when:
|
||||
@@ -81,7 +79,9 @@
|
||||
os_version: "{{ system_cfg.version | default('') | string }}"
|
||||
|
||||
- name: Normalize disk configuration (pre-computed system_cfg)
|
||||
when: system_cfg is defined
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- install_drive is not defined
|
||||
ansible.builtin.include_tasks: _normalize_disks.yml
|
||||
|
||||
# Runs on every path before validation, so an empty firewall.backend / content.source
|
||||
|
||||
@@ -317,7 +317,7 @@
|
||||
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _autologin is boolean and not _autologin or (_autologin is string and _autologin | length > 0 and _autologin in system_cfg.users | map(attribute='name'))
|
||||
- _autologin is boolean and not _autologin or (_autologin is string and _autologin | length > 0 and _autologin in system_cfg.users)
|
||||
fail_msg: >-
|
||||
desktop.autologin must be false or a username string present in
|
||||
system.users; got '{{ _autologin }}'. Bool true is not accepted - the
|
||||
@@ -355,12 +355,14 @@
|
||||
|
||||
- name: Validate at least one user with a password is defined
|
||||
vars:
|
||||
_pw_users: "{{ system_cfg.users | selectattr('password', 'defined') | map(attribute='password') | map('string') | reject('equalto', '') | list }}"
|
||||
_pw_users: "{{ system_cfg.users | dict2items | selectattr('value.password', 'defined') | list }}"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- system_cfg.users | default([]) | length > 0
|
||||
- system_cfg.users | default({}) | length > 0
|
||||
- _pw_users | length > 0
|
||||
fail_msg: "At least one user with a non-empty password must be defined in system.users."
|
||||
- _pw_users[0].key | string | length > 0
|
||||
- _pw_users[0].value.password | string | length > 0
|
||||
fail_msg: "At least one user with a password must be defined in system.users."
|
||||
quiet: true
|
||||
no_log: true
|
||||
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
# The virtualization role attaches system.disks to SCSI controller 0 in list order, and
|
||||
# vSphere never gives a disk unit 7 (the controller's own address). The kernel's sdX order
|
||||
# is not that order and differs between the install environment and the installed OS, so
|
||||
# a disk derived as /dev/sd<letter of its index> can be the wrong one. Resolve by target.
|
||||
- name: List disks with their SCSI address
|
||||
ansible.builtin.command: lsblk -dnpo NAME,TYPE,HCTL
|
||||
register: partitioning_vmware_lsblk
|
||||
changed_when: false
|
||||
|
||||
- name: Map SCSI targets to devices
|
||||
vars:
|
||||
_scsi: >-
|
||||
{%- set out = [] -%}
|
||||
{%- for line in partitioning_vmware_lsblk.stdout_lines -%}
|
||||
{%- set p = line.split() -%}
|
||||
{%- if (p | length) == 3 and p[1] == 'disk' and p[2] is match('^\d+:0:\d+:0$') -%}
|
||||
{%- set _ = out.append({'name': p[0], 'host': p[2].split(':')[0], 'target': p[2].split(':')[2]}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
ansible.builtin.set_fact:
|
||||
partitioning_vmware_hosts: "{{ _scsi | map(attribute='host') | unique | list }}"
|
||||
partitioning_vmware_targets: "{{ _scsi | items2dict(key_name='target', value_name='name') }}"
|
||||
partitioning_vmware_target_count: "{{ _scsi | length }}"
|
||||
|
||||
- name: Assert the system disks sit on one SCSI controller with unique targets
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- partitioning_vmware_hosts | length == 1
|
||||
- partitioning_vmware_targets | length == partitioning_vmware_target_count | int
|
||||
fail_msg: >-
|
||||
Expected every disk on one SCSI controller with unique targets, got
|
||||
{{ partitioning_vmware_lsblk.stdout_lines | select('search', ' disk ') | list }}.
|
||||
quiet: true
|
||||
|
||||
- name: Assert each system disk has a device at its SCSI target
|
||||
vars:
|
||||
_target: "{{ (idx if idx < 7 else idx + 1) | string }}"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _target in partitioning_vmware_targets
|
||||
fail_msg: "system.disks[{{ idx }}] ({{ item.size }} GB) expects a disk at SCSI target {{ _target }}, none found."
|
||||
quiet: true
|
||||
loop: "{{ system_cfg.disks }}"
|
||||
loop_control:
|
||||
index_var: idx
|
||||
label: "{{ idx }}"
|
||||
|
||||
- name: Point system disks and install_drive at the resolved devices
|
||||
vars:
|
||||
_disks: >-
|
||||
{%- set out = [] -%}
|
||||
{%- for d in system_cfg.disks -%}
|
||||
{%- set dev = partitioning_vmware_targets[(loop.index0 if loop.index0 < 7 else loop.index0 + 1) | string] -%}
|
||||
{%- set _ = out.append(d | combine({'device': dev, 'partition': dev ~ '1'})) -%}
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: "{{ system_cfg | combine({'disks': _disks}) }}"
|
||||
install_drive: "{{ _disks[0].device }}"
|
||||
@@ -1,10 +1,4 @@
|
||||
---
|
||||
- name: Resolve VMware disks by SCSI target
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.type == "virtual"
|
||||
ansible.builtin.include_tasks: _resolve_vmware_disks.yml
|
||||
|
||||
- name: Detect system sizing
|
||||
ansible.builtin.include_tasks: _detect_sizing.yml
|
||||
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
---
|
||||
- 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
|
||||
until: system_check_esxi.ansible_facts is defined
|
||||
retries: 3
|
||||
delay: 10
|
||||
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
|
||||
until: system_check_cluster.datastores is defined
|
||||
retries: 3
|
||||
delay: 10
|
||||
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
|
||||
# A missing file fails with status 404; a dropped vCenter connection fails with status None.
|
||||
until: system_check_media_files is not failed or system_check_media_files.status is not none
|
||||
retries: 3
|
||||
delay: 10
|
||||
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 }}"
|
||||
@@ -82,11 +82,6 @@
|
||||
name: "{{ hostname }}"
|
||||
folder: "{{ system_cfg.path if system_cfg.path | length > 0 else omit }}"
|
||||
register: system_check_vmware_check_result
|
||||
until: >-
|
||||
not system_check_vmware_check_result.failed
|
||||
or (system_check_vmware_check_result.msg is search('non-existing VM'))
|
||||
retries: 3
|
||||
delay: 10
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
no_log: true
|
||||
@@ -137,7 +132,3 @@
|
||||
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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
# Raw system.content input -> expected normalized system_cfg.content. Covers family
|
||||
# source defaults (rhel=dvd, else mirror), url pass-through, satellite inputs, and the
|
||||
# source defaults (EL=dvd, else mirror), url pass-through, satellite inputs, and the
|
||||
# Arch reflector lock-out (dropped only when a custom mirror url is set).
|
||||
content_fixtures:
|
||||
- name: arch-mirror-url-drops-reflector
|
||||
|
||||
Reference in New Issue
Block a user