Compare commits

...

13 Commits

Author SHA1 Message Date
b72816e985 fix(partitioning): add partition separator for NVMe/mmcblk device paths 2026-02-22 02:39:36 +01:00
ac0b5caf83 refactor(configuration): centralize DNS list variables in network dispatch 2026-02-22 02:39:32 +01:00
3ddc3c72ed refactor(configuration): extract shared BLS update task to reduce duplication 2026-02-22 02:39:28 +01:00
f1af7ccbca fix(bootstrap): add missing --best flag to RHEL dnf commands 2026-02-22 02:39:23 +01:00
51ca969ff4 refactor(global_defaults): consolidate hypervisor auth into shared credential dicts 2026-02-22 02:35:04 +01:00
1221249546 refactor(bootstrap,configuration,environment): add defaults/main.yml and extract hardcoded values 2026-02-22 02:32:36 +01:00
87fd69b825 refactor(bootstrap,configuration): add per-role _normalize.yml for platform resolution 2026-02-22 02:27:46 +01:00
3deb3ea751 refactor(configuration): add platform_config dict and replace is_rhel/is_debian with os_family lookups 2026-02-22 02:26:54 +01:00
cc30637f09 feat(global_defaults): add os_family_map and os_family fact for platform config lookups 2026-02-22 02:23:05 +01:00
23721aac96 fix(virtualization): add vTPM2 result validation before VMware power-on 2026-02-22 02:22:37 +01:00
5a9b346d72 feat(global_defaults): add semantic validations for IP, hostname, LUKS method, and interface prefix 2026-02-22 02:22:05 +01:00
75267e5140 refactor(global_defaults): extract physical_default_os to configurable default 2026-02-22 02:21:34 +01:00
f0fb68992d fix(global_defaults): normalize system.type 'vm' to 'virtual' for main project compatibility 2026-02-22 02:21:22 +01:00
36 changed files with 327 additions and 181 deletions

View File

@@ -0,0 +1,15 @@
---
# OS → task file mapping for bootstrap dispatch.
# Each key matches a supported `os` value; value is the task file to include.
bootstrap_os_task_map:
almalinux: _dnf_family.yml
alpine: alpine.yml
archlinux: archlinux.yml
debian: debian.yml
fedora: _dnf_family.yml
opensuse: opensuse.yml
rocky: _dnf_family.yml
rhel: rhel.yml
ubuntu: ubuntu.yml
ubuntu-lts: ubuntu.yml
void: void.yml

View File

@@ -0,0 +1,11 @@
---
# Resolve the OS-specific variable namespace and task file for the bootstrap role.
- name: Validate OS is supported for bootstrap
ansible.builtin.assert:
that:
- os is defined
- os in bootstrap_os_task_map
fail_msg: >-
Unsupported OS '{{ os | default("undefined") }}' for bootstrap.
Supported: {{ bootstrap_os_task_map | dict2items | map(attribute='key') | join(', ') }}
quiet: true

View File

@@ -1,6 +1,9 @@
---
- name: Normalize bootstrap
ansible.builtin.import_tasks: _normalize.yml
- name: Create API filesystem mountpoints in installroot
when: is_rhel | bool
when: os_family == 'RedHat'
ansible.builtin.file:
path: "/mnt/{{ item }}"
state: directory
@@ -11,7 +14,7 @@
- sys
- name: Mount API filesystems into installroot
when: is_rhel | bool
when: os_family == 'RedHat'
ansible.posix.mount:
src: "{{ item.src }}"
path: "/mnt/{{ item.path }}"
@@ -28,18 +31,6 @@
- name: Run OS-specific bootstrap process
vars:
bootstrap_os_task_map:
almalinux: _dnf_family.yml
alpine: alpine.yml
archlinux: archlinux.yml
debian: debian.yml
fedora: _dnf_family.yml
opensuse: opensuse.yml
rocky: _dnf_family.yml
rhel: rhel.yml
ubuntu: ubuntu.yml
ubuntu-lts: ubuntu.yml
void: void.yml
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"

View File

@@ -13,7 +13,7 @@
block:
- name: Install base packages in chroot environment
ansible.builtin.command: >-
dnf --releasever={{ os_version_major }} {{ _rhel_repos }}
dnf --releasever={{ os_version_major }} --best {{ _rhel_repos }}
--installroot=/mnt
--setopt=install_weak_deps=False --setopt=optional_metadata_types=filelists
groupinstall -y {{ _rhel_groups }}
@@ -51,7 +51,7 @@
- name: Install additional packages in chroot
ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version_major }}
{{ chroot_command }} dnf --releasever={{ os_version_major }} --best
--setopt=install_weak_deps=False install -y {{ _rhel_extra }}
register: bootstrap_result
changed_when: bootstrap_result.rc == 0

View File

@@ -4,15 +4,8 @@
delegate_to: localhost
become: false
module_defaults:
community.proxmox.proxmox_disk:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
community.proxmox.proxmox_kvm:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
community.proxmox.proxmox_disk: "{{ _proxmox_auth }}"
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
block:
- name: Cleanup Setup Disks
community.proxmox.proxmox_disk:

View File

@@ -88,11 +88,7 @@
delegate_to: localhost
become: false
module_defaults:
community.proxmox.proxmox_kvm:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
no_log: true
block:
- name: Stop Proxmox VM
@@ -113,11 +109,7 @@
delegate_to: localhost
become: false
module_defaults:
community.vmware.vmware_guest:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
community.vmware.vmware_guest: "{{ _vmware_auth }}"
no_log: true
block:
- name: Power off VMware VM

View File

@@ -4,18 +4,8 @@
delegate_to: localhost
become: false
module_defaults:
community.vmware.vmware_guest:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
vmware.vmware.vm_powerstate:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
community.vmware.vmware_guest: "{{ _vmware_auth }}"
vmware.vmware.vm_powerstate: "{{ _vmware_auth }}"
no_log: true
block:
- name: Remove CD-ROM from VM in vCenter

View File

@@ -0,0 +1,7 @@
---
# Network configuration dispatch — maps OS name to the task file
# that writes network config. Default (NetworkManager) applies to
# all OSes not explicitly listed.
configuration_network_task_map:
alpine: network_alpine.yml
void: network_void.yml

View File

@@ -0,0 +1,19 @@
---
# Shared task: update BLS (Boot Loader Specification) entries with kernel cmdline.
# Expects variable: _bls_cmdline (the kernel command line string)
- name: Find BLS entries
ansible.builtin.find:
paths: /mnt/boot/loader/entries
patterns: "*.conf"
register: _bls_entries
changed_when: false
- name: Update BLS options
when: _bls_entries.files | length > 0
ansible.builtin.lineinfile:
path: "{{ item.path }}"
regexp: "^options "
line: "options {{ _bls_cmdline }}"
loop: "{{ _bls_entries.files }}"
loop_control:
label: "{{ item.path }}"

View File

@@ -0,0 +1,16 @@
---
# Resolve platform-specific configuration for the target OS family.
# Sets _configuration_platform from configuration_platform_config[os_family].
- name: Resolve platform-specific configuration
ansible.builtin.assert:
that:
- os_family is defined
- os_family in configuration_platform_config
fail_msg: >-
Unsupported os_family '{{ os_family | default("undefined") }}'.
Extend configuration_platform_config in vars/main.yml.
quiet: true
- name: Set platform configuration
ansible.builtin.set_fact:
_configuration_platform: "{{ configuration_platform_config[os_family] }}"

View File

@@ -6,11 +6,10 @@
"redhat" if os == "rhel"
else ("ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os)
}}
_efi_loader: >-
{{ "shimx64.efi" if is_rhel | bool else "grubx64.efi" }}
_efi_loader: "{{ _configuration_platform.efi_loader }}"
block:
- name: Install GRUB EFI binary
when: not (is_rhel | bool)
when: _configuration_platform.grub_install
ansible.builtin.command: >-
{{ chroot_command }} /usr/sbin/grub-install --target=x86_64-efi
--efi-directory={{ partitioning_efi_mountpoint }}
@@ -44,20 +43,8 @@
backrefs: true
- name: Regenerate initramfs
when: os not in ["alpine", "void"]
vars:
configuration_initramfs_cmd: >-
{{
'/usr/sbin/mkinitcpio -P'
if os == "archlinux"
else (
'/usr/bin/env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin '
+ '/usr/sbin/update-initramfs -u -k all'
if is_debian | bool
else '/usr/bin/dracut --regenerate-all --force'
)
}}
ansible.builtin.command: "{{ chroot_command }} {{ configuration_initramfs_cmd }}"
when: _configuration_platform.initramfs_cmd | length > 0
ansible.builtin.command: "{{ chroot_command }} {{ _configuration_platform.initramfs_cmd }}"
register: configuration_initramfs_result
changed_when: configuration_initramfs_result.rc == 0
@@ -65,10 +52,10 @@
vars:
configuration_grub_cfg_cmd: >-
{{
'/usr/sbin/grub2-mkconfig -o '
'/usr/sbin/' + _configuration_platform.grub_mkconfig_prefix + ' -o '
+ partitioning_efi_mountpoint
+ '/EFI/' + _efi_vendor + '/grub.cfg'
if is_rhel | bool
if os_family == 'RedHat'
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
}}
ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}"

View File

@@ -134,7 +134,7 @@
- name: Ensure keyfile pattern for initramfs-tools
when:
- is_debian | bool
- os_family == 'Debian'
- configuration_luks_keyfile_in_use
ansible.builtin.lineinfile:
path: /mnt/etc/cryptsetup-initramfs/conf-hook
@@ -198,14 +198,14 @@
}})
- name: Ensure dracut config directory exists
when: is_rhel | bool
when: os_family == 'RedHat'
ansible.builtin.file:
path: /mnt/etc/dracut.conf.d
state: directory
mode: "0755"
- name: Configure dracut for LUKS
when: is_rhel | bool
when: os_family == 'RedHat'
ansible.builtin.copy:
dest: /mnt/etc/dracut.conf.d/crypt.conf
content: |
@@ -216,13 +216,13 @@
mode: "0644"
- name: Read kernel cmdline defaults
when: is_rhel | bool
when: os_family == 'RedHat'
ansible.builtin.slurp:
src: /mnt/etc/kernel/cmdline
register: configuration_kernel_cmdline_slurp
- name: Build kernel cmdline with LUKS args
when: is_rhel | bool
when: os_family == 'RedHat'
vars:
kernel_cmdline_current: >-
{{ configuration_kernel_cmdline_slurp.content | b64decode | trim }}
@@ -247,40 +247,26 @@
configuration_kernel_cmdline_new: "{{ kernel_cmdline_new }}"
- name: Write kernel cmdline with LUKS args
when: is_rhel | bool
when: os_family == 'RedHat'
ansible.builtin.copy:
dest: /mnt/etc/kernel/cmdline
mode: "0644"
content: "{{ configuration_kernel_cmdline_new }}\n"
- name: Find BLS entries for encryption kernel cmdline
when: is_rhel | bool
ansible.builtin.find:
paths: /mnt/boot/loader/entries
patterns: "*.conf"
register: configuration_kernel_bls_entries
changed_when: false
- name: Update BLS options with LUKS args
when:
- is_rhel | bool
- configuration_kernel_bls_entries.files | length > 0
ansible.builtin.lineinfile:
path: "{{ item.path }}"
regexp: "^options "
line: "options {{ configuration_kernel_cmdline_new }}"
loop: "{{ configuration_kernel_bls_entries.files }}"
loop_control:
label: "{{ item.path }}"
- name: Update BLS entries with LUKS kernel cmdline
when: os_family == 'RedHat'
vars:
_bls_cmdline: "{{ configuration_kernel_cmdline_new }}"
ansible.builtin.include_tasks: _bls_update.yml
- name: Read grub defaults
when: not is_rhel | bool
when: not os_family == 'RedHat'
ansible.builtin.slurp:
src: /mnt/etc/default/grub
register: configuration_grub_slurp
- name: Build grub command lines with LUKS args
when: not is_rhel | bool
when: not os_family == 'RedHat'
vars:
grub_content: "{{ configuration_grub_slurp.content | b64decode }}"
grub_cmdline_linux: >-
@@ -344,7 +330,7 @@
configuration_grub_cmdline_default_new: "{{ grub_cmdline_default_new }}"
- name: Update GRUB_CMDLINE_LINUX_DEFAULT for LUKS
when: not is_rhel | bool
when: not os_family == 'RedHat'
ansible.builtin.lineinfile:
path: /mnt/etc/default/grub
regexp: "^GRUB_CMDLINE_LINUX_DEFAULT="

View File

@@ -1,7 +1,7 @@
---
- name: Append vim configurations to vimrc
ansible.builtin.blockinfile:
path: "{{ '/mnt/etc/vim/vimrc' if is_debian | bool else '/mnt/etc/vimrc' }}"
path: "{{ '/mnt/etc/vim/vimrc' if os_family == 'Debian' else '/mnt/etc/vimrc' }}"
block: |
set encoding=utf-8
set number

View File

@@ -1,6 +1,6 @@
---
- name: Configure grub defaults
when: not is_rhel | bool
when: os_family != 'RedHat'
ansible.builtin.lineinfile:
dest: /mnt/etc/default/grub
regexp: "{{ item.regexp }}"
@@ -14,7 +14,7 @@
label: "{{ item.line }}"
- name: Ensure grub defaults file exists for RHEL-based systems
when: is_rhel | bool
when: os_family == 'RedHat'
block:
- name: Build RHEL kernel command line defaults
vars:
@@ -96,22 +96,10 @@
mode: "0644"
content: "{{ configuration_kernel_cmdline_base }}\n"
- name: Find BLS entries for GRUB configuration
ansible.builtin.find:
paths: /mnt/boot/loader/entries
patterns: "*.conf"
register: configuration_grub_bls_entries
changed_when: false
- name: Update BLS options with kernel cmdline defaults
when: configuration_grub_bls_entries.files | length > 0
ansible.builtin.lineinfile:
path: "{{ item.path }}"
regexp: "^options "
line: "options {{ configuration_kernel_cmdline_base }}"
loop: "{{ configuration_grub_bls_entries.files }}"
loop_control:
label: "{{ item.path }}"
- name: Update BLS entries with kernel cmdline defaults
vars:
_bls_cmdline: "{{ configuration_kernel_cmdline_base }}"
ansible.builtin.include_tasks: _bls_update.yml
- name: Enable GRUB cryptodisk for encrypted /boot
when: partitioning_grub_enable_cryptodisk | bool

View File

@@ -14,7 +14,7 @@
- name: Setup locales
block:
- name: Configure locale.gen
when: not is_rhel | bool
when: _configuration_platform.locale_gen
ansible.builtin.lineinfile:
dest: /mnt/etc/locale.gen
regexp: "{{ item.regex }}"
@@ -25,7 +25,7 @@
label: "{{ item.line }}"
- name: Generate locales
when: not is_rhel | bool
when: _configuration_platform.locale_gen
ansible.builtin.command: "{{ chroot_command }} /usr/sbin/locale-gen"
register: configuration_locale_result
changed_when: configuration_locale_result.rc == 0

View File

@@ -1,4 +1,7 @@
---
- name: Normalize configuration
ansible.builtin.import_tasks: _normalize.yml
- name: Include configuration tasks
when: configuration_task.when | default(true)
ansible.builtin.include_tasks: "{{ configuration_task.file }}"
@@ -17,7 +20,7 @@
- file: users.yml
- file: sudo.yml
- file: selinux.yml
when: "{{ is_rhel | bool }}"
when: "{{ os_family == 'RedHat' }}"
loop_control:
loop_var: configuration_task
label: "{{ configuration_task.file }}"

View File

@@ -29,9 +29,10 @@
- configuration_detected_interfaces | length > 0
fail_msg: Failed to detect any network interfaces.
- name: Set DNS configuration facts
ansible.builtin.set_fact:
configuration_dns_list: "{{ system_cfg.network.dns.servers }}"
configuration_dns_search: "{{ system_cfg.network.dns.search }}"
- name: Configure networking
vars:
configuration_network_task_map:
alpine: network_alpine.yml
void: network_void.yml
ansible.builtin.include_tasks: "{{ configuration_network_task_map[os] | default('network_nm.yml') }}"

View File

@@ -1,7 +1,5 @@
---
- name: Write Alpine network interfaces
vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
ansible.builtin.copy:
dest: /mnt/etc/network/interfaces
mode: "0644"
@@ -25,9 +23,6 @@
{% endfor %}
- name: Set Alpine DNS resolvers
vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
configuration_dns_search: "{{ system_cfg.network.dns.search | default([]) }}"
when: configuration_dns_list | length > 0 or configuration_dns_search | length > 0
ansible.builtin.copy:
dest: /mnt/etc/resolv.conf

View File

@@ -1,8 +1,5 @@
---
- name: Write dhcpcd configuration
vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
configuration_dns_search: "{{ system_cfg.network.dns.search | default([]) }}"
ansible.builtin.copy:
dest: /mnt/etc/dhcpcd.conf
mode: "0644"

View File

@@ -1,6 +1,6 @@
---
- name: Fix SELinux
when: is_rhel | bool
when: os_family == 'RedHat'
block:
- name: Fix SELinux by pre-labeling the filesystem before first boot
when: os in ['almalinux', 'rocky', 'rhel'] and system_cfg.features.selinux.enabled | bool

View File

@@ -1,13 +1,13 @@
---
- name: Enable systemd services
when: os not in ['alpine', 'void']
when: _configuration_platform.init_system == 'systemd'
vars:
configuration_systemd_services: >-
{{
['NetworkManager']
+ (['firewalld'] if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else [])
+ (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else [])
+ ([('ssh' if is_debian | bool else 'sshd')] if system_cfg.features.ssh.enabled | bool else [])
+ ([_configuration_platform.ssh_service] if system_cfg.features.ssh.enabled | bool else [])
+ (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' else [])
}}
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ item }}"
@@ -16,7 +16,7 @@
changed_when: configuration_enable_service_result.rc == 0
- name: Enable OpenRC services
when: os == 'alpine'
when: _configuration_platform.init_system == 'openrc'
vars:
configuration_openrc_services: >-
{{
@@ -48,7 +48,7 @@
when: item.stat.exists
- name: Enable runit services
when: os == 'void'
when: _configuration_platform.init_system == 'runit'
vars:
configuration_runit_services: >-
{{

View File

@@ -9,7 +9,7 @@
- name: Give sudo access to wheel group
ansible.builtin.copy:
content: "{{ '%sudo ALL=(ALL) ALL\n' if is_debian | bool else '%wheel ALL=(ALL) ALL\n' }}"
content: "{{ _configuration_platform.sudo_group }} ALL=(ALL) ALL\n"
dest: /mnt/etc/sudoers.d/01-wheel
mode: "0440"
validate: /usr/sbin/visudo --check --file=%s

View File

@@ -17,8 +17,7 @@
- name: Create user accounts
vars:
configuration_user_group: >-
{{ "sudo" if is_debian | bool else "wheel" }}
configuration_user_group: "{{ _configuration_platform.user_group }}"
# UID starts at 1000; safe for fresh installs only
configuration_useradd_cmd: >-
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group

View File

@@ -9,8 +9,8 @@ interface-name={{ configuration_iface_name }}
[ipv4]
{% set iface = configuration_iface %}
{% set dns_list = system_cfg.network.dns.servers | default([]) %}
{% set search_list = system_cfg.network.dns.search | default([]) %}
{% set dns_list = configuration_dns_list %}
{% set search_list = configuration_dns_search %}
{% if iface.ip | default('') | string | length %}
address1={{ iface.ip }}/{{ iface.prefix }}{{ (',' ~ iface.gateway) if (iface.gateway | default('') | string | length) else '' }}
method=manual

View File

@@ -0,0 +1,67 @@
---
# Platform-specific configuration values keyed by os_family.
# Consumed as _configuration_platform in tasks via:
# configuration_platform_config[os_family]
configuration_platform_config:
RedHat:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: shimx64.efi
grub_install: false
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
grub_mkconfig_prefix: grub2-mkconfig
locale_gen: false
init_system: systemd
Debian:
user_group: sudo
sudo_group: "%sudo"
ssh_service: ssh
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: >-
/usr/bin/env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/usr/sbin/update-initramfs -u -k all
grub_mkconfig_prefix: grub-mkconfig
locale_gen: true
init_system: systemd
Archlinux:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: "/usr/sbin/mkinitcpio -P"
grub_mkconfig_prefix: grub-mkconfig
locale_gen: true
init_system: systemd
Suse:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
grub_mkconfig_prefix: grub-mkconfig
locale_gen: true
init_system: systemd
Alpine:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: ""
grub_mkconfig_prefix: grub-mkconfig
locale_gen: false
init_system: openrc
Void:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: ""
grub_mkconfig_prefix: grub-mkconfig
locale_gen: false
init_system: runit

View File

@@ -0,0 +1,10 @@
---
# Connection and timing
environment_wait_timeout: 180
environment_wait_delay: 5
# Pacman installer settings
environment_parallel_downloads: 20
environment_pacman_lock_timeout: 120
environment_pacman_retries: 4
environment_pacman_retry_delay: 15

View File

@@ -1,8 +1,8 @@
---
- name: Wait for connection
ansible.builtin.wait_for_connection:
timeout: 180
delay: 5
timeout: "{{ environment_wait_timeout }}"
delay: "{{ environment_wait_delay }}"
- name: Gather facts
ansible.builtin.setup:

View File

@@ -4,14 +4,14 @@
ansible.builtin.lineinfile:
path: /etc/pacman.conf
regexp: ^#ParallelDownloads =
line: ParallelDownloads = 20
line: "ParallelDownloads = {{ environment_parallel_downloads }}"
- name: Wait for pacman lock to be released
when: not (custom_iso | bool)
ansible.builtin.wait_for:
path: /var/lib/pacman/db.lck
state: absent
timeout: 120
timeout: "{{ environment_pacman_lock_timeout }}"
changed_when: false
- name: Setup Pacman
@@ -31,8 +31,8 @@
- { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] }
loop_control:
label: "{{ item.name }}"
retries: 4
delay: 15
retries: "{{ environment_pacman_retries }}"
delay: "{{ environment_pacman_retry_delay }}"
- name: Prepare /iso mount and repository for RHEL-based systems
when: os == "rhel"

View File

@@ -9,6 +9,22 @@ os_family_debian:
- debian
- ubuntu
- ubuntu-lts
# OS → family mapping — aligns with the main project's ansible_os_family pattern.
# Enables platform_config dict lookups per role instead of inline when: is_rhel chains.
os_family_map:
almalinux: RedHat
alpine: Alpine
archlinux: Archlinux
debian: Debian
fedora: RedHat
opensuse: Suse
rhel: RedHat
rocky: RedHat
ubuntu: Debian
ubuntu-lts: Debian
void: Void
os_supported:
- almalinux
- alpine
@@ -34,9 +50,11 @@ hypervisor_defaults:
storage: ""
datacenter: ""
cluster: ""
folder: ""
certs: false
ssh: false
physical_default_os: "archlinux"
custom_iso: false
thirdparty_tasks: "dropins/preparation.yml"

View File

@@ -2,7 +2,7 @@
- name: Build normalized system configuration
vars:
system_raw: "{{ system_defaults | combine(system, recursive=True) }}"
system_type: "{{ system_raw.type | string | lower }}"
system_type: "{{ 'virtual' if (system_raw.type | string | lower) in ['vm', 'virtual'] else (system_raw.type | string | lower) }}"
system_os_input: "{{ system_raw.os | default('') | string | lower }}"
system_name: >-
{{
@@ -14,7 +14,7 @@
system_cfg:
# --- Identity & platform ---
type: "{{ system_type }}"
os: "{{ system_os_input if system_os_input | length > 0 else ('archlinux' if system_type == 'physical' else '') }}"
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
version: "{{ system_raw.version | default('') | string }}"
filesystem: "{{ system_raw.filesystem | default('') | string | lower }}"
name: "{{ system_name }}"
@@ -64,9 +64,9 @@
)
}}
# --- Locale & environment ---
timezone: "{{ system_raw.timezone | default('Europe/Vienna') | string }}"
locale: "{{ system_raw.locale | default('en_US.UTF-8') | string }}"
keymap: "{{ system_raw.keymap | default('us') | string }}"
timezone: "{{ system_raw.timezone | string }}"
locale: "{{ system_raw.locale | string }}"
keymap: "{{ system_raw.keymap | string }}"
path: "{{ system_raw.path | default('') | string }}"
packages: >-
{{
@@ -129,7 +129,7 @@
chroot:
tool: "{{ system_raw.features.chroot.tool | string }}"
hostname: "{{ system_name }}"
os: "{{ system_os_input if system_os_input | length > 0 else ('archlinux' if system_type == 'physical' else '') }}"
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
os_version: "{{ system_raw.version | default('') | string }}"
no_log: true

View File

@@ -10,6 +10,31 @@
- name: Normalize hypervisor inputs
ansible.builtin.include_tasks: hypervisor.yml
- name: Set VMware module auth defaults
when: hypervisor_type == 'vmware'
ansible.builtin.set_fact:
_vmware_auth:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
no_log: true
- name: Set Proxmox module auth defaults
when: hypervisor_type == 'proxmox'
ansible.builtin.set_fact:
_proxmox_auth:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
_proxmox_auth_node:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
no_log: true
- name: Normalize system inputs
ansible.builtin.include_tasks: system.yml
@@ -20,6 +45,7 @@
ansible.builtin.set_fact:
is_rhel: "{{ os in os_family_rhel }}"
is_debian: "{{ os in os_family_debian }}"
os_family: "{{ os_family_map[os] | default('Unknown') }}"
- name: Normalize OS version for keying
when:

View File

@@ -347,8 +347,48 @@
that:
- item is mapping
- item.bridge is defined and (item.bridge | string | length) > 0
fail_msg: "Each system.network.interfaces[] entry must be a dict with at least a 'bridge' key."
- >-
(item.ip | default('') | string | length) == 0
or (item.prefix | default('') | string | length) > 0
fail_msg: "Each system.network.interfaces[] entry must have a 'bridge' key and 'prefix' when 'ip' is set."
quiet: true
loop: "{{ system_cfg.network.interfaces }}"
loop_control:
label: "{{ item | to_json }}"
- name: Validate hostname format
ansible.builtin.assert:
that:
- hostname is regex("^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$")
fail_msg: "hostname '{{ hostname }}' contains invalid characters. Use only alphanumeric, hyphens, dots, and underscores."
quiet: true
- name: Validate IP address format
when: system_cfg.network.ip is defined and (system_cfg.network.ip | string | length) > 0
ansible.builtin.assert:
that:
- system_cfg.network.ip is regex("^([0-9]{1,3}\\.){3}[0-9]{1,3}$")
fail_msg: "system.network.ip '{{ system_cfg.network.ip }}' is not a valid IPv4 address."
quiet: true
- name: Validate DNS server format
when:
- system_cfg.network.dns.servers is defined
- system_cfg.network.dns.servers | length > 0
ansible.builtin.assert:
that:
- item is regex("^([0-9]{1,3}\\.){3}[0-9]{1,3}$")
fail_msg: "DNS server '{{ item }}' is not a valid IPv4 address."
quiet: true
loop: "{{ system_cfg.network.dns.servers }}"
- name: Validate LUKS method
when: system_cfg.luks.enabled | bool
ansible.builtin.assert:
that:
- system_cfg.luks.method in ["tpm2", "keyfile"]
- >-
(system_cfg.luks.passphrase | string | length) > 0
fail_msg: "system.luks.method must be 'tpm2' or 'keyfile', and luks.passphrase must be set when LUKS is enabled."
quiet: true
no_log: true

View File

@@ -1,5 +1,8 @@
---
partitioning_btrfs_compress_opt: "{{ 'compress=zstd:15' if system_cfg.features.zstd.enabled | bool else '' }}"
# Partition separator: 'p' for NVMe/mmcblk (device path ends in digit), empty for SCSI/virtio.
# Examples: /dev/sda → /dev/sda1, /dev/nvme0n1 → /dev/nvme0n1p1
partitioning_part_sep: "{{ 'p' if (install_drive | default('') | regex_search('\\d$')) else '' }}"
partitioning_boot_partition_suffix: 1
partitioning_main_partition_suffix: 2
partitioning_efi_size_mib: 512
@@ -113,12 +116,12 @@ partitioning_grub_enable_cryptodisk: >-
and not (partitioning_separate_boot | bool)
and (partitioning_efi_mountpoint == '/boot/efi')
}}
partitioning_luks_device: "{{ install_drive ~ (partitioning_root_partition_suffix | string) }}"
partitioning_luks_device: "{{ install_drive ~ partitioning_part_sep ~ (partitioning_root_partition_suffix | string) }}"
partitioning_root_device: >-
{{
'/dev/mapper/' + system_cfg.luks.mapper
if (system_cfg.luks.enabled | bool)
else install_drive ~ (partitioning_root_partition_suffix | string)
else install_drive ~ partitioning_part_sep ~ (partitioning_root_partition_suffix | string)
}}
partitioning_disk_size_gb: >-
{{

View File

@@ -3,7 +3,7 @@
block:
- name: Create FAT32 filesystem in boot partition
community.general.filesystem:
dev: "{{ install_drive }}{{ partitioning_boot_partition_suffix }}"
dev: "{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_partition_suffix }}"
fstype: vfat
opts: -F32 -n BOOT
force: true
@@ -11,7 +11,7 @@
- name: Create filesystem for /boot partition
when: partitioning_separate_boot | bool
community.general.filesystem:
dev: "{{ install_drive }}{{ partitioning_boot_fs_partition_suffix }}"
dev: "{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
fstype: "{{ partitioning_boot_fs_fstype }}"
opts: "{{ '-m bigtime=0 -i nrext64=0,exchange=0 -n parent=0' if (is_rhel | bool and partitioning_boot_fs_fstype == 'xfs') else omit }}"
force: true
@@ -23,7 +23,7 @@
- os in ['almalinux', 'rocky', 'rhel'] or (os == 'debian' and (os_version | string) == '11')
ansible.builtin.command: >-
tune2fs -O "^orphan_file,^metadata_csum_seed"
"{{ install_drive }}{{ partitioning_boot_fs_partition_suffix }}"
"{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
register: partitioning_boot_ext4_tune_result
changed_when: false
@@ -39,7 +39,7 @@
ansible.builtin.include_tasks: "{{ system_cfg.filesystem }}.yml"
- name: Get UUID for boot filesystem
ansible.builtin.command: blkid -s UUID -o value '{{ install_drive }}{{ partitioning_boot_partition_suffix }}'
ansible.builtin.command: blkid -s UUID -o value '{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_partition_suffix }}'
register: partitioning_boot_uuid
changed_when: false
failed_when: partitioning_boot_uuid.rc != 0 or (partitioning_boot_uuid.stdout | trim | length) == 0
@@ -47,7 +47,7 @@
- name: Get UUID for /boot filesystem
when: partitioning_separate_boot | bool
ansible.builtin.command: >-
blkid -s UUID -o value '{{ install_drive }}{{ partitioning_boot_fs_partition_suffix }}'
blkid -s UUID -o value '{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}'
register: partitioning_boot_fs_uuid
changed_when: false
failed_when: partitioning_boot_fs_uuid.rc != 0 or (partitioning_boot_fs_uuid.stdout | trim | length) == 0

View File

@@ -2,11 +2,7 @@
- name: Deploy VM on Proxmox
delegate_to: localhost
module_defaults:
community.proxmox.proxmox_kvm:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
block:
- name: Create VM on Proxmox
vars:

View File

@@ -14,24 +14,9 @@
- name: Deploy VM in vCenter
delegate_to: localhost
module_defaults:
community.vmware.vmware_guest:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
community.vmware.vmware_guest_tpm:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
vmware.vmware.vm_powerstate:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
community.vmware.vmware_guest: "{{ _vmware_auth }}"
community.vmware.vmware_guest_tpm: "{{ _vmware_auth }}"
vmware.vmware.vm_powerstate: "{{ _vmware_auth }}"
block:
# community.vmware: full-featured guest management
- name: Create VM in vCenter
@@ -100,6 +85,17 @@
name: "{{ hostname }}"
state: present
no_log: true
register: virtualization_vmware_tpm2_result
- name: Validate vTPM2 was added successfully
when:
- virtualization_tpm2_enabled | bool
- virtualization_vmware_tpm2_result is defined
ansible.builtin.assert:
that:
- virtualization_vmware_tpm2_result is not failed
fail_msg: "Failed to add vTPM2 to VM '{{ hostname }}'. LUKS with TPM2 requires a virtual TPM device."
quiet: true
# vmware.vmware: modern collection for power operations
- name: Start VM in vCenter