Compare commits

..

68 Commits

Author SHA1 Message Date
4fff9f8d80 fix(virtualization): create vmware target folder before vm deploy 2026-06-04 18:17:50 +02:00
7f12a0f3d8 feat: accept proxmox API-token auth alongside password 2026-05-31 12:40:31 +02:00
ceb2237bbb fix(encryption): add tpm2-tss dracut module explicitly for TPM2 LUKS 2026-05-31 12:39:24 +02:00
477c8379c4 fix(configuration): enable per-family time-sync and skip sudo-rs lecture 2026-05-31 12:30:26 +02:00
579c499c02 feat(configuration): multi-backend networking, bind by match not MAC 2026-05-31 12:25:53 +02:00
89e366d0f0 fix: EL10 PAM and crypto readiness via authselect profile and DEFAULT policy 2026-05-28 17:30:57 +02:00
6fe843355e fix(bootstrap): keep package cache off the 2 GiB CIS /var during install 2026-05-28 17:26:25 +02:00
441876fab9 refactor(global_defaults): single source of truth for family-default resolution 2026-05-28 17:25:23 +02:00
00acd4d200 refactor(configuration): consolidate firewall into one phase-aware path 2026-05-27 05:28:00 +02:00
d922efd2e4 feat: uniform system.content source schema across installers and repositories 2026-05-27 05:15:32 +02:00
939c5c741f feat: golden-image build support (cloud-init on EL, selinux relabel, SSH wait) 2026-05-27 05:05:55 +02:00
2c35409519 feat(cis): add selectable profile and per-rule hardening toggles 2026-05-25 04:37:33 +02:00
d2a19cfd5c feat(hardware): auto-detect audio, bluetooth, camera with declarative override 2026-05-25 04:36:21 +02:00
44f5adc682 feat(bootstrap): per-os desktop apps, KDE plasma-login-manager and DM resolution 2026-05-25 04:30:53 +02:00
0185797af9 fix(environment): co-upgrade soname closure when installing installer tools 2026-05-25 03:54:12 +02:00
e0ecf628cd fix(bootstrap): deploy all non-EOL core distros (keyrings, repos, versions) 2026-05-25 03:52:44 +02:00
37df881daa docs: refresh bootstrap examples and README 2026-05-30 18:05:14 +02:00
55b21eae5d fix: encryption, partitioning, cis and virtualization hardening 2026-05-30 18:05:14 +02:00
b1e938b7f0 fix(users): accept plaintext or pre-hashed passwords uniformly 2026-05-30 18:05:05 +02:00
c843f5289b feat: hardware/firmware/gpu/peripherals detection and packages 2026-05-30 18:05:05 +02:00
9757ed3785 feat: complete wayland desktop deployment (gnome/kde/sway/hyprland) 2026-05-30 18:05:05 +02:00
876e90ce2b refactor: trim bootstrap OS support to core three distro families 2026-05-30 18:04:00 +02:00
7c44cb1ff0 docs(bootstrap): fix users dict format in examples, sync schema defaults, document secure_boot/rhel_repo 2026-05-30 09:25:34 +02:00
5d0630a386 refactor(global_defaults): drop orphan luks.urandom/verify and aur feature, bump fedora to 45 2026-05-30 09:25:34 +02:00
3eaf918a53 fix(lint): convert sshd restart to handler, add pipefail to btrfs subvol set 2026-05-30 09:25:34 +02:00
382e82ff85 fix(configuration): tolerate missing units, gate Secure Boot to supported OSes, fix clevis install per family 2026-05-30 09:25:34 +02:00
db7dc53bd7 docs(bootstrap): document firmware/gpu/peripherals/hardware features 2026-05-30 09:25:34 +02:00
7d45f25a7e feat(bootstrap): install vendor-matched hardware packages 2026-05-30 09:25:34 +02:00
3880b8f41e feat(environment): detect cpu/gpu/wireless/fingerprint hardware 2026-05-30 09:25:34 +02:00
dc3c4a901f feat(global_defaults): firmware/gpu/peripherals/hardware schema 2026-05-30 09:25:34 +02:00
6bfaa0aa2b fix(configuration): guard user keys access to avoid dict.keys() method collision 2026-04-16 15:37:13 +02:00
5dc0424acb feat(cleanup): enroll Secure Boot keys in VM NVRAM after OS installation 2026-04-16 15:37:13 +02:00
c12332a853 feat(virtualization): enable TPM2 emulation for Secure Boot VMs 2026-04-16 15:37:13 +02:00
5ace4577a2 feat(configuration): auto-bind PCR 7 when Secure Boot and FDE are both enabled 2026-04-16 15:37:13 +02:00
c0ff01ef40 feat(configuration): add Secure Boot tasks for shim and sbctl 2026-04-16 15:37:13 +02:00
57417514e3 feat(configuration): override EFI loader to shim when Secure Boot enabled 2026-04-02 04:34:47 +02:00
0928588c1f feat(bootstrap): add Secure Boot conditional packages for Debian, Ubuntu, and Arch 2026-04-02 04:34:16 +02:00
6d622f2db4 feat(global_defaults): add secure_boot feature toggle with normalization 2026-04-02 04:33:07 +02:00
b11d65a6f3 docs(bootstrap): document desktop, initramfs, and FDE features with SSH keepalive config 2026-04-01 15:07:58 +02:00
3623fc292c feat(configuration): generic FDE with systemd-cryptenroll, clevis fallback, and configurable initramfs 2026-04-01 15:07:58 +02:00
dfca7ec94b fix(configuration): RedHat EFI grub wrapper with btrfs subvol prefix and boot order 2026-04-01 15:07:58 +02:00
e8be84bf49 fix(partitioning): set btrfs default subvolume and restrict @pkg to Arch 2026-04-01 15:07:58 +02:00
322cc0b1ce fix(bootstrap): resolve interface-only network, sshd penalties, dnf scriptlets, and EFI cleanup 2026-04-01 15:07:58 +02:00
4b38754f8b feat(bootstrap): add desktop environment support with configurable DE, DM, and display target 2026-04-01 15:07:58 +02:00
a6bc7ffe04 fix(configuration): use /boot/grub2/grub.cfg for RedHat EFI grub config 2026-03-25 16:03:39 +01:00
c529e71ebc feat(packages): add needrestart to Debian and Ubuntu package lists 2026-03-20 18:06:14 +01:00
cb46de2b6d feat(bootstrap): add full package upgrade step for Debian and Ubuntu 2026-03-20 18:05:04 +01:00
9169117b25 fix(vim): use vimscript comment syntax for blockinfile markers in vimrc 2026-03-20 18:00:12 +01:00
6c94c519fb fix(sudo): use explicit string check instead of bool conditional for sudo field 2026-03-20 17:31:49 +01:00
efd96a42b8 fix(connection): set ansible_port explicitly at every connection transition 2026-03-20 17:31:49 +01:00
68661c3cca fix(vmware): use primary ansible_* vars for vmware_tools connection plugin precedence 2026-03-20 17:31:49 +01:00
1db20c7ac0 fix(vmware): use empty password for vmware_tools during live ISO bootstrap 2026-03-20 17:31:49 +01:00
7b155b427b fix(users): update cloud-init template and input validation for dict users 2026-03-20 17:31:49 +01:00
ca8721e98f refactor(prompts): remove vars_prompt, require users defined in inventory 2026-03-20 17:31:49 +01:00
cdb2559d8f fix(prompts): add default values to vars_prompt to skip in non-interactive mode 2026-03-20 17:31:49 +01:00
443f6623df refactor(users): change system.users from list to dict keyed by username 2026-03-20 17:31:49 +01:00
6cf418fe00 fix(configuration): make root password, user keys, and sudo all optional 2026-03-20 17:31:49 +01:00
47ec5fe621 fix(cloud-init): handle missing keys and make sudo conditional 2026-03-20 17:31:49 +01:00
240f945cce fix(cleanup): remove ansible_become override that blocks swapoff/umount 2026-03-20 17:31:49 +01:00
663a04556f feat(global_defaults): add system.features.aur schema for validation passthrough 2026-03-20 17:31:49 +01:00
6febd1acf1 refactor(virtualization): extract shared Xen disk definitions 2026-03-12 12:27:18 +01:00
008187860c refactor: remove unnecessary changed_when from set_fact tasks 2026-03-12 12:25:45 +01:00
cd1be6b5e1 refactor(partitioning): remove redundant blockdev --rereadpt calls 2026-03-12 12:25:15 +01:00
15be6149fd refactor(partitioning): remove unused register variables 2026-03-12 12:24:59 +01:00
ca29ad200d chore: suppress args[module] false positives from variable-based module_defaults 2026-03-12 12:12:27 +01:00
8079099cee fix(cleanup): add no_log to Proxmox VM restart task 2026-03-12 12:12:27 +01:00
9e79185b07 fix(virtualization): add missing changed_when to Xen VM stop task 2026-03-12 12:12:27 +01:00
b88bf2860f fix(configuration): replace fail+ignore_errors with debug for TPM2 fallback warning 2026-03-12 12:12:27 +01:00
24 changed files with 56 additions and 476 deletions

View File

@@ -183,7 +183,7 @@ Top-level variables outside `system`/`hypervisor`.
#### `system.content` #### `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 | | 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. `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 | | Key | Type | Description |
| ------------- | ------ | ------------------------------------------------------ | | ------------- | ------ | ------------------------------------------------------ |
| `size` | number | Disk size in GB (required for virtual) | | `size` | number | Disk size in GB (required for virtual) |

View File

@@ -9,7 +9,7 @@ bootstrap_hardware_packages:
amd: [amd-ucode] amd: [amd-ucode]
firmware_base: [] firmware_base: []
firmware: 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] amd: [linux-firmware-amdgpu]
nvidia: [linux-firmware-nvidia] nvidia: [linux-firmware-nvidia]
atheros: [linux-firmware-atheros] atheros: [linux-firmware-atheros]

View File

@@ -206,7 +206,6 @@ bootstrap_debian:
- openssh-server - openssh-server
- python3 - python3
- xfsprogs - xfsprogs
- xz-utils
extra: extra:
- bat - bat
- curl - curl
@@ -259,7 +258,6 @@ bootstrap_ubuntu:
- openssh-server - openssh-server
- python3 - python3
- xfsprogs - xfsprogs
- xz-utils
extra: extra:
- bash-completion - bash-completion
- bat - bat

View File

@@ -3,5 +3,3 @@
cleanup_verify_boot: true cleanup_verify_boot: true
cleanup_boot_timeout: 300 cleanup_boot_timeout: 300
cleanup_remove_on_failure: true cleanup_remove_on_failure: true
cleanup_poweroff_retries: 24
cleanup_poweroff_delay: 5

View File

@@ -6,30 +6,7 @@
module_defaults: module_defaults:
community.proxmox.proxmox_disk: "{{ _proxmox_auth }}" community.proxmox.proxmox_disk: "{{ _proxmox_auth }}"
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}" community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
community.proxmox.proxmox_vm_info: "{{ _proxmox_auth_node }}"
block: 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 - name: Cleanup Setup Disks
community.proxmox.proxmox_disk: community.proxmox.proxmox_disk:
name: "{{ hostname }}" name: "{{ hostname }}"
@@ -44,8 +21,8 @@
failed_when: false failed_when: false
no_log: true no_log: true
- name: Boot the installed OS - name: Start the VM
community.proxmox.proxmox_kvm: community.proxmox.proxmox_kvm:
vmid: "{{ system_cfg.id }}" vmid: "{{ system_cfg.id }}"
state: started state: restarted
no_log: true no_log: true

View File

@@ -5,26 +5,9 @@
become: false become: false
module_defaults: module_defaults:
community.vmware.vmware_guest: "{{ _vmware_auth }}" community.vmware.vmware_guest: "{{ _vmware_auth }}"
community.vmware.vmware_guest_info: "{{ _vmware_auth }}"
vmware.vmware.vm_powerstate: "{{ _vmware_auth }}" vmware.vmware.vm_powerstate: "{{ _vmware_auth }}"
no_log: true no_log: true
block: 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 - name: Remove CD-ROM from VM in vCenter
community.vmware.vmware_guest: community.vmware.vmware_guest:
name: "{{ hostname }}" name: "{{ hostname }}"
@@ -58,7 +41,7 @@
}} }}
failed_when: false failed_when: false
- name: Boot the installed OS - name: Start VM in vCenter
vmware.vmware.vm_powerstate: vmware.vmware.vm_powerstate:
name: "{{ hostname }}" name: "{{ hostname }}"
state: powered-on state: powered-on

View File

@@ -18,24 +18,13 @@
register: configuration_bootloader_result register: configuration_bootloader_result
changed_when: configuration_bootloader_result.rc == 0 changed_when: configuration_bootloader_result.rc == 0
- name: Remove stale EFI boot entries for this OS - name: Check existing EFI boot entries
# UEFI NVRAM entries survive a disk wipe (they live in firmware, not on disk). ansible.builtin.command: efibootmgr
# A prior install leaves a dead entry, and a case-insensitive duplicate label register: configuration_efi_entries
# ("ArchLinux" vs "archlinux") would otherwise win the boot order - the firmware changed_when: false
# 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: 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: >- ansible.builtin.command: >-
efibootmgr -c efibootmgr -c
-L '{{ _efi_vendor }}' -L '{{ _efi_vendor }}'
@@ -45,16 +34,15 @@
register: configuration_efi_entry_result register: configuration_efi_entry_result
changed_when: configuration_efi_entry_result.rc == 0 changed_when: configuration_efi_entry_result.rc == 0
- name: Install removable EFI fallback loader - name: Set installed OS as first EFI boot entry
# Firmware-agnostic fallback (\EFI\BOOT\BOOTX64.EFI): many consumer firmwares ansible.builtin.shell:
# ignore or reset custom NVRAM entries and boot only the removable path. cmd: >-
when: _configuration_platform.grub_install set -o pipefail &&
ansible.builtin.command: >- efibootmgr | grep -i '{{ _efi_vendor }}' | grep -oP 'Boot\K[0-9A-F]+' | head -1
{{ chroot_command }} /usr/bin/install -Dm644 | xargs -I{} efibootmgr -o {}
{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/{{ _efi_loader }} executable: /bin/bash
{{ partitioning_efi_mountpoint }}/EFI/BOOT/BOOTX64.EFI register: _efi_bootorder_result
register: _efi_fallback_result changed_when: _efi_bootorder_result.rc == 0
changed_when: _efi_fallback_result.rc == 0
- name: Ensure lvm2 for non btrfs filesystems - name: Ensure lvm2 for non btrfs filesystems
when: os == "archlinux" and system_cfg.filesystem != "btrfs" when: os == "archlinux" and system_cfg.filesystem != "btrfs"

View File

@@ -12,21 +12,6 @@
and 'No such file or directory' not in (_firewall_enable.stderr | default('')) and 'No such file or directory' not in (_firewall_enable.stderr | default(''))
and 'does not exist' 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), # 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. # so its activation and SSH rule are applied here, after reboot.
- name: Allow SSH through ufw before enabling - name: Allow SSH through ufw before enabling

View File

@@ -43,14 +43,11 @@
}} }}
grub_root_flags: >- grub_root_flags: >-
{{ ['rootflags=subvol=@'] if system_cfg.filesystem == 'btrfs' else [] }} {{ ['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 # 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. # jinja render trips on list+list when grub_lvm_args leads the expression here.
grub_cmdline_linux_base: >- grub_cmdline_linux_base: >-
{{ {{
((grub_lvm_args | join(' ')) ~ ' ' ~ (grub_selinux_args | join(' ')) ~ ' ' ((grub_lvm_args | join(' ')) ~ ' ' ~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
}} }}
grub_kernel_cmdline_base: >- grub_kernel_cmdline_base: >-
{{ {{
@@ -60,7 +57,6 @@
+ ['ro'] + ['ro']
+ grub_lvm_args + grub_lvm_args
+ grub_root_flags + grub_root_flags
+ grub_selinux_args
+ (_hardware_profile_kernel_params | default([])) + (_hardware_profile_kernel_params | default([]))
) )
| join(' ') | join(' ')

View File

@@ -37,6 +37,7 @@
org_id: "{{ system_cfg.content.satellite.org }}" org_id: "{{ system_cfg.content.satellite.org }}"
activationkey: "{{ system_cfg.content.satellite.activation_key }}" activationkey: "{{ system_cfg.content.satellite.activation_key }}"
environment: "{{ system_cfg.content.satellite.environment | default(omit, true) }}" environment: "{{ system_cfg.content.satellite.environment | default(omit, true) }}"
auto_attach: true
force_register: true force_register: true
server_proxy_hostname: "{{ (system_cfg.content.proxy | urlsplit('hostname')) | default(omit, 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) }}" server_proxy_port: "{{ (system_cfg.content.proxy | urlsplit('port')) | default(omit, true) }}"

View File

@@ -22,12 +22,10 @@
mode: "0644" mode: "0644"
# Fedora: setfiles segfaults during bootstrap chroot relabeling, so SELinux # Fedora: setfiles segfaults during bootstrap chroot relabeling, so SELinux
# is left permissive and expected to relabel on first boot. Opting out writes # is left permissive and expected to relabel on first boot.
# disabled here and selinux=0 on the kernel cmdline (grub.yml), the only form - name: Disable SELinux
# EL9 honours.
- name: Set the SELinux mode
when: os == "fedora" or not system_cfg.features.selinux.enabled | bool when: os == "fedora" or not system_cfg.features.selinux.enabled | bool
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: /mnt/etc/selinux/config path: /mnt/etc/selinux/config
regexp: ^SELINUX= regexp: ^SELINUX=
line: "SELINUX={{ 'permissive' if system_cfg.features.selinux.enabled | bool else 'disabled' }}" line: SELINUX=permissive

View File

@@ -54,18 +54,6 @@
and 'No such file or directory' not in (configuration_enable_service_result.stderr | default('')) 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('')) 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 - name: Enable display manager for selected desktop
when: when:
- _configuration_platform.init_system == 'systemd' - _configuration_platform.init_system == 'systemd'

View File

@@ -15,7 +15,8 @@
validate: /usr/sbin/visudo --check --file=%s validate: /usr/sbin/visudo --check --file=%s
- name: Deploy per-user sudoers rules - 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: vars:
configuration_sudoers_rule: >- configuration_sudoers_rule: >-
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }} {{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}

View File

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

View File

@@ -73,25 +73,28 @@
mode: "0755" mode: "0755"
- name: Detect RHEL ISO device - name: Detect RHEL ISO device
ansible.builtin.command: lsblk -rbno NAME,TYPE,SIZE ansible.builtin.command: lsblk -rno NAME,TYPE
register: environment_lsblk_result register: environment_lsblk_result
changed_when: false changed_when: false
- name: Select RHEL ISO device - name: Select RHEL ISO device
vars: vars:
_roms: >- _rom_devices: >-
{%- set out = [] -%} {{
{%- for line in environment_lsblk_result.stdout_lines -%} environment_lsblk_result.stdout_lines
{%- set p = line.split() -%} | map('split', ' ')
{%- if (p | length) >= 3 and p[1] == 'rom' -%} | selectattr('1', 'equalto', 'rom')
{%- set _ = out.append({'name': p[0], 'size': p[2] | int}) -%} | map('first')
{%- endif -%} | map('regex_replace', '^', '/dev/')
{%- endfor -%} | list
{{ out }} }}
ansible.builtin.set_fact: ansible.builtin.set_fact:
environment_rhel_iso_device: >- 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 - name: Mount RHEL ISO
ansible.posix.mount: ansible.posix.mount:

View File

@@ -78,8 +78,7 @@ system_defaults:
timezone: "Europe/Vienna" timezone: "Europe/Vienna"
locale: "en_US.UTF-8" locale: "en_US.UTF-8"
keymap: "us" keymap: "us"
# source: dvd|mirror|satellite|none ('' -> family default: rhel=dvd, else mirror; # source: dvd|mirror|satellite|none ('' -> family default: EL=dvd, else mirror).
# almalinux/rocky/fedora have public mirrors, rhel does not).
# satellite values come from inventory/vault only, never committed code. # satellite values come from inventory/vault only, never committed code.
content: content:
source: "" source: ""

View File

@@ -94,7 +94,6 @@
- name: Set install_drive from primary disk - name: Set install_drive from primary disk
when: when:
- install_drive is not defined
- system_disks_cfg | length > 0 - system_disks_cfg | length > 0
- system_disks_cfg[0].device | string | length > 0 - system_disks_cfg[0].device | string | length > 0
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@@ -35,12 +35,10 @@
}, recursive=True) }, 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 - name: Check if pre-computed system_cfg needs enrichment
when: system_cfg is defined when: system_cfg is defined
ansible.builtin.set_fact: 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 - name: Merge pre-computed system_cfg with bootstrap system_defaults
when: when:
@@ -81,7 +79,9 @@
os_version: "{{ system_cfg.version | default('') | string }}" os_version: "{{ system_cfg.version | default('') | string }}"
- name: Normalize disk configuration (pre-computed system_cfg) - 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 ansible.builtin.include_tasks: _normalize_disks.yml
# Runs on every path before validation, so an empty firewall.backend / content.source # Runs on every path before validation, so an empty firewall.backend / content.source

View File

@@ -317,7 +317,7 @@
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}" _autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
ansible.builtin.assert: ansible.builtin.assert:
that: 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: >- fail_msg: >-
desktop.autologin must be false or a username string present in desktop.autologin must be false or a username string present in
system.users; got '{{ _autologin }}'. Bool true is not accepted - the 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 - name: Validate at least one user with a password is defined
vars: 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: ansible.builtin.assert:
that: that:
- system_cfg.users | default([]) | length > 0 - system_cfg.users | default({}) | length > 0
- _pw_users | 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 quiet: true
no_log: true no_log: true

View File

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

View File

@@ -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 - name: Detect system sizing
ansible.builtin.include_tasks: _detect_sizing.yml ansible.builtin.include_tasks: _detect_sizing.yml

View File

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

View File

@@ -82,11 +82,6 @@
name: "{{ hostname }}" name: "{{ hostname }}"
folder: "{{ system_cfg.path if system_cfg.path | length > 0 else omit }}" folder: "{{ system_cfg.path if system_cfg.path | length > 0 else omit }}"
register: system_check_vmware_check_result 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 failed_when: false
changed_when: false changed_when: false
no_log: true no_log: true
@@ -137,7 +132,3 @@
To avoid data loss, the playbook will not overwrite or delete existing VMs. 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. Please choose a different hostname or remove the existing VM manually before proceeding.
quiet: true quiet: true
- name: Verify boot media and placement target
when: system_cfg.type == "virtual"
ansible.builtin.include_tasks: _placement.yml

View File

@@ -1,6 +1,6 @@
--- ---
# Raw system.content input -> expected normalized system_cfg.content. Covers family # 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). # Arch reflector lock-out (dropped only when a custom mirror url is set).
content_fixtures: content_fixtures:
- name: arch-mirror-url-drops-reflector - name: arch-mirror-url-drops-reflector