--- - name: Ensure system input is a dictionary ansible.builtin.set_fact: system: "{{ system | default({}) }}" changed_when: false - name: Validate system input type ansible.builtin.assert: that: - system is mapping fail_msg: "system must be a dictionary" quiet: true - name: Build normalized system configuration vars: system_raw: "{{ system_defaults | combine(system, recursive=True) }}" system_type: "{{ system_raw.type | string | lower }}" system_os_input: "{{ system_raw.os | default('') | string | lower }}" system_name: >- {{ system_raw.name | string | trim if (system_raw.name | default('') | string | trim | length) > 0 else inventory_hostname }} system_dns: >- {{ system_defaults.dns | combine((system_raw.dns if system_raw.dns is mapping else {}), recursive=True) }} system_user: >- {{ system_defaults.user | combine((system_raw.user if system_raw.user is mapping else {}), recursive=True) }} system_root: >- {{ system_defaults.root | combine((system_raw.root if system_raw.root is mapping else {}), recursive=True) }} system_luks: >- {{ system_defaults.luks | combine((system_raw.luks if system_raw.luks is mapping else {}), recursive=True) }} system_luks_tpm2: >- {{ system_defaults.luks.tpm2 | combine((system_luks.tpm2 if system_luks.tpm2 is mapping else {}), recursive=True) }} system_features: >- {{ system_defaults.features | combine((system_raw.features if system_raw.features is mapping else {}), recursive=True) }} system_feature_cis: >- {{ system_defaults.features.cis | combine((system_features.cis if system_features.cis is mapping else {}), recursive=True) }} system_feature_selinux: >- {{ system_defaults.features.selinux | combine((system_features.selinux if system_features.selinux is mapping else {}), recursive=True) }} system_feature_firewall: >- {{ system_defaults.features.firewall | combine((system_features.firewall if system_features.firewall is mapping else {}), recursive=True) }} system_feature_ssh: >- {{ system_defaults.features.ssh | combine((system_features.ssh if system_features.ssh is mapping else {}), recursive=True) }} system_feature_zstd: >- {{ system_defaults.features.zstd | combine((system_features.zstd if system_features.zstd is mapping else {}), recursive=True) }} system_feature_swap: >- {{ system_defaults.features.swap | combine((system_features.swap if system_features.swap is mapping else {}), recursive=True) }} system_feature_banner: >- {{ system_defaults.features.banner | combine((system_features.banner if system_features.banner is mapping else {}), recursive=True) }} system_feature_chroot: >- {{ system_defaults.features.chroot | combine((system_features.chroot if system_features.chroot is mapping else {}), recursive=True) }} system_dns_servers_input: "{{ system_dns.servers | default([]) }}" system_dns_search_input: "{{ system_dns.search | default([]) }}" system_packages_input: "{{ system_raw.packages | default([]) }}" ansible.builtin.set_fact: system_cfg: type: "{{ system_type }}" os: "{{ system_os_input if system_os_input | length > 0 else ('archlinux' if system_type == 'physical' else '') }}" version: "{{ system_raw.version | default('') | string }}" name: "{{ system_name }}" id: "{{ system_raw.id | default('') | string }}" cpus: "{{ [system_raw.cpus | default(0) | int, 0] | max }}" memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}" balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}" network: "{{ system_raw.network | default('') | string }}" vlan: "{{ system_raw.vlan | default('') | string }}" ip: "{{ system_raw.ip | default('') | string }}" prefix: >- {{ (system_raw.prefix | int) if (system_raw.prefix | default('') | string | length) > 0 else '' }} gateway: "{{ system_raw.gateway | default('') | string }}" dns: servers: >- {{ ( system_dns_servers_input if system_dns_servers_input is iterable and system_dns_servers_input is not string else (system_dns_servers_input | string).split(',') ) | map('trim') | reject('equalto', '') | list }} search: >- {{ ( system_dns_search_input if system_dns_search_input is iterable and system_dns_search_input is not string else (system_dns_search_input | string).split(',') ) | map('trim') | reject('equalto', '') | list }} path: "{{ system_raw.path | default('') | string }}" packages: >- {{ ( system_packages_input if system_packages_input is iterable and system_packages_input is not string else (system_packages_input | string).split(',') ) | map('trim') | reject('equalto', '') | list }} disks: "{{ system_raw.disks | default([]) }}" user: name: "{{ system_user.name | string }}" password: "{{ system_user.password | string }}" key: "{{ system_user.key | string }}" root: password: "{{ system_root.password | string }}" luks: enabled: "{{ system_luks.enabled | bool }}" passphrase: "{{ system_luks.passphrase | string }}" mapper: "{{ system_luks.mapper | string }}" auto: "{{ system_luks.auto | bool }}" method: "{{ system_luks.method | string | lower }}" tpm2: device: "{{ system_luks_tpm2.device | string }}" pcrs: "{{ system_luks_tpm2.pcrs | string }}" keysize: "{{ system_luks.keysize | int }}" options: "{{ system_luks.options | string }}" type: "{{ system_luks.type | string }}" cipher: "{{ system_luks.cipher | string }}" hash: "{{ system_luks.hash | string }}" iter: "{{ system_luks.iter | int }}" bits: "{{ system_luks.bits | int }}" pbkdf: "{{ system_luks.pbkdf | string }}" urandom: "{{ system_luks.urandom | bool }}" verify: "{{ system_luks.verify | bool }}" features: cis: enabled: "{{ system_feature_cis.enabled | bool }}" selinux: enabled: "{{ system_feature_selinux.enabled | bool }}" firewall: enabled: "{{ system_feature_firewall.enabled | bool }}" backend: "{{ system_feature_firewall.backend | string | lower }}" toolkit: "{{ system_feature_firewall.toolkit | string | lower }}" ssh: enabled: "{{ system_feature_ssh.enabled | bool }}" zstd: enabled: "{{ system_feature_zstd.enabled | bool }}" swap: enabled: "{{ system_feature_swap.enabled | bool }}" banner: motd: "{{ system_feature_banner.motd | bool }}" sudo: "{{ system_feature_banner.sudo | bool }}" chroot: tool: "{{ system_feature_chroot.tool | string }}" hostname: "{{ system_name }}" os: "{{ system_os_input if system_os_input | length > 0 else ('archlinux' if system_type == 'physical' else '') }}" os_version: "{{ system_raw.version | default('') | string }}" changed_when: false - name: Normalize system disks input vars: system_disks: "{{ system_cfg.disks | default([]) }}" system_disk_letter_map: "abcdefghijklmnopqrstuvwxyz" system_disk_device_prefix: >- {{ '/dev/vd' if system_cfg.type == 'virtual' and hypervisor_type == 'libvirt' else ( '/dev/xvd' if system_cfg.type == 'virtual' and hypervisor_type == 'xen' else ( '/dev/sd' if system_cfg.type == 'virtual' and hypervisor_type in ['proxmox', 'vmware'] else '' ) ) }} block: - name: Validate system disks type ansible.builtin.assert: that: - system_disks is sequence fail_msg: "system.disks must be a list" quiet: true - name: Validate system disk items ansible.builtin.assert: that: - item is mapping fail_msg: "Each system disk entry must be a dictionary" quiet: true loop: "{{ system_disks }}" loop_control: label: "{{ item | to_json }}" - name: Validate system disk mount schema ansible.builtin.assert: that: - item.mount is not defined or item.mount is mapping fail_msg: "system.disks[].mount must be a dictionary (e.g. mount: {path: /data})." quiet: true loop: "{{ system_disks }}" loop_control: label: "{{ item | to_json }}" - name: Validate system disk count ansible.builtin.assert: that: - (system_disks | length) <= 26 fail_msg: "system.disks supports at most 26 entries." quiet: true - name: Initialize normalized disk list ansible.builtin.set_fact: system_disks_cfg: [] changed_when: false - name: Build normalized system disk configuration vars: disk_idx: "{{ ansible_loop.index0 }}" disk_letter: "{{ system_disk_letter_map[disk_idx] }}" disk_cfg_base: "{{ system_disk_defaults | combine(item, recursive=True) }}" disk_mount: "{{ system_disk_defaults.mount | combine((disk_cfg_base.mount | default({})), recursive=True) }}" disk_mount_path: "{{ (disk_mount.path | default('') | string) | trim }}" disk_mount_fstype: >- {{ disk_mount.fstype if (disk_mount.fstype | default('') | string | length) > 0 else ('ext4' if disk_mount_path | length > 0 else '') }} disk_device: >- {{ disk_cfg_base.device if (disk_cfg_base.device | string | length) > 0 else ( (system_disk_device_prefix ~ disk_letter) if system_cfg.type == 'virtual' else '' ) }} disk_partition: >- {{ disk_device ~ ('p1' if (disk_device | regex_search('\\d$')) else '1') if disk_device | length > 0 else '' }} ansible.builtin.set_fact: system_disks_cfg: >- {{ system_disks_cfg + [ disk_cfg_base | combine( { 'device': disk_device, 'mount': { 'path': disk_mount_path, 'fstype': disk_mount_fstype, 'label': disk_mount.label | default('') | string, 'opts': disk_mount.opts | default('defaults') | string }, 'partition': disk_partition }, recursive=True ) ] }} loop: "{{ system_disks }}" loop_control: loop_var: item extended: true label: "{{ item | to_json }}" - name: Update system configuration with normalized disks ansible.builtin.set_fact: system_cfg: "{{ system_cfg | combine({'disks': system_disks_cfg}, recursive=True) }}" changed_when: false - name: Set install_drive from primary disk when: - system_disks_cfg | length > 0 - system_disks_cfg[0].device | string | length > 0 ansible.builtin.set_fact: install_drive: "{{ system_disks_cfg[0].device }}" changed_when: false