From 961c8f259c4588f24f1ac5447a79e5835afff32c Mon Sep 17 00:00:00 2001 From: Sandwich Date: Wed, 11 Feb 2026 05:37:18 +0100 Subject: [PATCH] refactor(vars): enforce nested system and hypervisor schema --- main.yml | 92 +++++-- roles/global_defaults/defaults/main.yml | 93 ++++--- roles/global_defaults/tasks/main.yml | 12 +- roles/global_defaults/tasks/system.yml | 288 +++++++++++++++++---- roles/global_defaults/tasks/validation.yml | 283 ++++++++++++++++---- 5 files changed, 606 insertions(+), 162 deletions(-) diff --git a/main.yml b/main.yml index 7548260..56400f3 100644 --- a/main.yml +++ b/main.yml @@ -5,26 +5,75 @@ gather_facts: false become: true vars_prompt: - - name: user_name + - name: system_user_name prompt: | What is your username? private: false - - name: user_public_key + - name: system_user_public_key prompt: | What is your ssh key? private: false - - name: user_password + - name: system_user_password prompt: | What is your password? confirm: true - - name: root_password + - name: system_root_password prompt: | What is your root password? confirm: true pre_tasks: + - name: Apply prompted authentication values to system input + vars: + system_input: "{{ system | default({}) }}" + system_user_input: "{{ (system_input.user | default({})) if (system_input.user is mapping) else {} }}" + system_root_input: "{{ (system_input.root | default({})) if (system_input.root is mapping) else {} }}" + system_user_name_effective: >- + {{ + (system_user_input.name | default('') | string) + if (system_user_input.name | default('') | string | length) > 0 + else (system_user_name | default('') | string) + }} + system_user_public_key_effective: >- + {{ + (system_user_input.public_key | default('') | string) + if (system_user_input.public_key | default('') | string | length) > 0 + else (system_user_public_key | default('') | string) + }} + system_user_password_effective: >- + {{ + (system_user_input.password | default('') | string) + if (system_user_input.password | default('') | string | length) > 0 + else (system_user_password | default('') | string) + }} + system_root_password_effective: >- + {{ + (system_root_input.password | default('') | string) + if (system_root_input.password | default('') | string | length) > 0 + else (system_root_password | default('') | string) + }} + ansible.builtin.set_fact: + system: >- + {{ + system_input + | combine( + { + 'user': { + 'name': system_user_name_effective, + 'public_key': system_user_public_key_effective, + 'password': system_user_password_effective + }, + 'root': { + 'password': system_root_password_effective + } + }, + recursive=True + ) + }} + changed_when: false + - name: Load global defaults ansible.builtin.import_role: name: global_defaults @@ -35,7 +84,7 @@ roles: - role: virtualization - when: install_type == "virtual" + when: system_cfg.type == "virtual" become: false vars: ansible_connection: local @@ -54,10 +103,10 @@ - role: configuration - role: cis - when: cis_enabled + when: system_cfg.features.cis.enabled | bool - role: cleanup - when: install_type in ["virtual", "physical"] + when: system_cfg.type in ["virtual", "physical"] become: false post_tasks: @@ -68,7 +117,7 @@ (ansible_connection | default('ssh')) != 'ssh' or ((system_cfg.ip | default('') | string | length) > 0) or ( - install_type == 'physical' + system_cfg.type == 'physical' and (ansible_host | default('') | string | length) > 0 ) }} @@ -78,29 +127,16 @@ when: - post_reboot_can_connect | bool ansible.builtin.set_fact: - ansible_user: "{{ user_name }}" - ansible_password: "{{ user_password }}" - ansible_become_password: "{{ user_password }}" + ansible_user: "{{ system_cfg.user.name }}" + ansible_password: "{{ system_cfg.user.password }}" + ansible_become_password: "{{ system_cfg.user.password }}" ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" - - name: Install post-reboot extra packages - vars: - post_install_extra_packages: >- - {{ - ( - extra_packages - if (extra_packages is iterable and extra_packages is not string) - else (extra_packages | string).split(',') - ) - | map('trim') - | reject('equalto', '') - | list - }} + - name: Install post-reboot packages when: - post_reboot_can_connect | bool - - extra_packages is defined - - extra_packages | length > 0 - - post_install_extra_packages | length > 0 + - system_cfg.packages is defined + - system_cfg.packages | length > 0 ansible.builtin.package: - name: "{{ post_install_extra_packages }}" + name: "{{ system_cfg.packages }}" state: present diff --git a/roles/global_defaults/defaults/main.yml b/roles/global_defaults/defaults/main.yml index 628c8da..61c2686 100644 --- a/roles/global_defaults/defaults/main.yml +++ b/roles/global_defaults/defaults/main.yml @@ -7,58 +7,77 @@ hypervisor_defaults: url: "" username: "" password: "" - node: "" + host: "" storage: "" datacenter: "" cluster: "" validate_certs: false + ssh: false + custom_iso: false -cis: false -selinux: true -vmware_ssh: false -firewall_enabled: true -firewall_backend: "firewalld" -firewall_toolkit: "nftables" -ssh_enabled: true -zstd_enabled: true -swap_enabled: true -chroot_tool: "arch-chroot" -os_version: "" -motd_enabled: true -sudo_banner_enabled: true thirdparty_preparation_tasks_path: "dropins/preparation.yml" -cis_enabled: "{{ cis | bool }}" - system_defaults: + type: "virtual" # virtual|physical + os: "" + os_version: "" name: "" id: "" cpus: 0 - memory_mb: 0 - balloon_mb: 0 + memory: 0 # MiB + balloon: 0 # MiB network: "" vlan: "" ip: "" prefix: "" gateway: "" - dns_servers: [] - dns_search: [] + dns: + servers: [] + search: [] path: "" + packages: [] disks: [] - -luks_enabled: false -luks_mapper_name: "SYSTEM_DECRYPTED" -luks_auto_decrypt: true -luks_auto_decrypt_method: "tpm2" -luks_tpm2_device: "auto" -luks_tpm2_pcrs: "" -luks_keyfile_size: 64 -luks_options: "discard,tries=3" -luks_type: "luks2" -luks_cipher: "aes-xts-plain64" -luks_hash: "sha512" -luks_iter_time: 4000 -luks_key_size: 512 -luks_pbkdf: "argon2id" -luks_use_urandom: true -luks_verify_passphrase: true + user: + name: "" + password: "" + public_key: "" + root: + password: "" + luks: + enabled: false + passphrase: "" + mapper_name: "SYSTEM_DECRYPTED" + auto_decrypt: true + auto_decrypt_method: "tpm2" + tpm2_device: "auto" + tpm2_pcrs: "" + keyfile_size: 64 + options: "discard,tries=3" + type: "luks2" + cipher: "aes-xts-plain64" + hash: "sha512" + iter_time: 4000 + key_size: 512 + pbkdf: "argon2id" + use_urandom: true + verify_passphrase: true + features: + cis: + enabled: false + selinux: + enabled: true + firewall: + enabled: true + backend: "firewalld" # firewalld|ufw + toolkit: "nftables" # nftables|iptables + ssh: + enabled: true + zstd: + enabled: true + swap: + enabled: true + banner: + motd: true + sudo: true + chroot: + tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn diff --git a/roles/global_defaults/tasks/main.yml b/roles/global_defaults/tasks/main.yml index 09d5d57..6eab95c 100644 --- a/roles/global_defaults/tasks/main.yml +++ b/roles/global_defaults/tasks/main.yml @@ -48,8 +48,8 @@ chroot_command: >- {{ 'systemd-nspawn -D /mnt' - if chroot_tool == 'systemd-nspawn' - else chroot_tool ~ ' /mnt' + if (system_cfg.features.chroot.tool | default('arch-chroot')) == 'systemd-nspawn' + else (system_cfg.features.chroot.tool | default('arch-chroot')) ~ ' /mnt' }} changed_when: false @@ -63,12 +63,12 @@ - name: Set SSH access when: - - install_type == "virtual" + - system_cfg.type == "virtual" - hypervisor_type != "vmware" ansible.builtin.set_fact: - ansible_user: "{{ user_name }}" - ansible_password: "{{ user_password }}" - ansible_become_password: "{{ user_password }}" + ansible_user: "{{ system_cfg.user.name }}" + ansible_password: "{{ system_cfg.user.password }}" + ansible_become_password: "{{ system_cfg.user.password }}" ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" changed_when: false diff --git a/roles/global_defaults/tasks/system.yml b/roles/global_defaults/tasks/system.yml index 7af764d..fb095bb 100644 --- a/roles/global_defaults/tasks/system.yml +++ b/roles/global_defaults/tasks/system.yml @@ -13,12 +13,30 @@ - name: Normalize base system fields vars: + system_type_effective: >- + {{ + system.type + if system.type is defined and (system.type | string | length) > 0 + else system_defaults.type + }} system_name_effective: >- {{ system.name if system.name is defined and (system.name | string | length) > 0 else inventory_hostname }} + system_os_effective: >- + {{ + system.os + if system.os is defined and (system.os | string | length) > 0 + else '' + }} + system_os_version_effective: >- + {{ + system.os_version + if system.os_version is defined and (system.os_version | string | length) > 0 + else '' + }} system_id_effective: >- {{ system.id @@ -31,16 +49,16 @@ if system.cpus is defined and (system.cpus | int) > 0 else 0 }} - system_memory_mb_effective: >- + system_memory_effective: >- {{ - system.memory_mb - if system.memory_mb is defined and (system.memory_mb | int) > 0 + system.memory + if system.memory is defined and (system.memory | int) > 0 else 0 }} - system_balloon_mb_effective: >- + system_balloon_effective: >- {{ - system.balloon_mb - if system.balloon_mb is defined and (system.balloon_mb | int) > 0 + system.balloon + if system.balloon is defined and (system.balloon | int) > 0 else 0 }} system_network_effective: >- @@ -73,17 +91,38 @@ if system.gateway is defined and (system.gateway | string | length) > 0 else '' }} - system_dns_servers_effective: >- + system_dns_effective: >- {{ - system.dns_servers - if system.dns_servers is defined - else [] + (system_defaults.dns | default({'servers': [], 'search': []})) + | combine((system.dns if system.dns is defined else {}), recursive=True) }} - system_dns_search_effective: >- + system_dns_resolvers_value: "{{ system_dns_effective.servers if system_dns_effective.servers is defined else [] }}" + system_dns_resolvers_list_raw: >- {{ - system.dns_search - if system.dns_search is defined - else [] + system_dns_resolvers_value + if system_dns_resolvers_value is iterable and system_dns_resolvers_value is not string + else (system_dns_resolvers_value | string).split(',') + }} + system_dns_resolvers_effective: >- + {{ + system_dns_resolvers_list_raw + | map('trim') + | reject('equalto', '') + | list + }} + system_dns_domains_value: "{{ system_dns_effective.search if system_dns_effective.search is defined else [] }}" + system_dns_domains_list_raw: >- + {{ + system_dns_domains_value + if system_dns_domains_value is iterable and system_dns_domains_value is not string + else (system_dns_domains_value | string).split(',') + }} + system_dns_domains_effective: >- + {{ + system_dns_domains_list_raw + | map('trim') + | reject('equalto', '') + | list }} system_path_effective: >- {{ @@ -91,27 +130,169 @@ if system.path is defined and (system.path | string | length) > 0 else '' }} + system_user_effective: >- + {{ + (system_defaults.user | default({})) + | combine( + (system.user if (system.user is defined and system.user is mapping) else {}), + recursive=True + ) + }} + system_root_effective: >- + {{ + (system_defaults.root | default({})) + | combine( + (system.root if (system.root is defined and system.root is mapping) else {}), + recursive=True + ) + }} + system_luks_effective: >- + {{ + (system_defaults.luks | default({})) + | combine( + (system.luks if (system.luks is defined and system.luks is mapping) else {}), + recursive=True + ) + }} + system_features_effective: >- + {{ + (system_defaults.features | default({})) + | combine( + (system.features if (system.features is defined and system.features is mapping) else {}), + recursive=True + ) + }} + system_user_name_effective: "{{ (system_user_effective.name | default('') | string) }}" + system_user_password_effective: "{{ (system_user_effective.password | default('') | string) }}" + system_user_public_key_effective: "{{ (system_user_effective.public_key | default('') | string) }}" + system_root_password_effective: "{{ (system_root_effective.password | default('') | string) }}" + system_luks_passphrase_effective: "{{ (system_luks_effective.passphrase | default('') | string) }}" + system_luks_mapper_name_effective: "{{ (system_luks_effective.mapper_name | default('SYSTEM_DECRYPTED') | string) }}" + system_luks_auto_decrypt_method_effective: "{{ (system_luks_effective.auto_decrypt_method | default('tpm2') | string | lower) }}" + system_luks_tpm2_device_effective: "{{ (system_luks_effective.tpm2_device | default('auto') | string) }}" + system_luks_tpm2_pcrs_effective: "{{ (system_luks_effective.tpm2_pcrs | default('') | string) }}" + system_luks_options_effective: "{{ (system_luks_effective.options | default('discard,tries=3') | string) }}" + system_luks_type_effective: "{{ (system_luks_effective.type | default('luks2') | string) }}" + system_luks_cipher_effective: "{{ (system_luks_effective.cipher | default('aes-xts-plain64') | string) }}" + system_luks_hash_effective: "{{ (system_luks_effective.hash | default('sha512') | string) }}" + system_luks_iter_time_effective: "{{ (system_luks_effective.iter_time | default(4000) | int) }}" + system_luks_key_size_effective: "{{ (system_luks_effective.key_size | default(512) | int) }}" + system_luks_pbkdf_effective: "{{ (system_luks_effective.pbkdf | default('argon2id') | string) }}" + system_luks_keyfile_size_effective: "{{ (system_luks_effective.keyfile_size | default(64) | int) }}" + system_luks_enabled_effective: "{{ system_luks_effective.enabled | default(false) | bool }}" + system_luks_auto_decrypt_effective: "{{ system_luks_effective.auto_decrypt | default(true) | bool }}" + system_luks_use_urandom_effective: "{{ system_luks_effective.use_urandom | default(true) | bool }}" + system_luks_verify_passphrase_effective: "{{ system_luks_effective.verify_passphrase | default(true) | bool }}" + system_features_cis_enabled_effective: "{{ system_features_effective.cis.enabled | default(false) | bool }}" + system_features_selinux_enabled_effective: "{{ system_features_effective.selinux.enabled | default(true) | bool }}" + system_features_firewall_enabled_effective: "{{ system_features_effective.firewall.enabled | default(true) | bool }}" + system_features_firewall_backend_effective: "{{ (system_features_effective.firewall.backend | default('firewalld') | string | lower) }}" + system_features_firewall_toolkit_effective: "{{ (system_features_effective.firewall.toolkit | default('nftables') | string | lower) }}" + system_features_ssh_enabled_effective: "{{ system_features_effective.ssh.enabled | default(true) | bool }}" + system_features_zstd_enabled_effective: "{{ system_features_effective.zstd.enabled | default(true) | bool }}" + system_features_swap_enabled_effective: "{{ system_features_effective.swap.enabled | default(true) | bool }}" + system_features_banner_motd_effective: "{{ system_features_effective.banner.motd | default(true) | bool }}" + system_features_banner_sudo_effective: "{{ system_features_effective.banner.sudo | default(true) | bool }}" + system_features_chroot_tool_effective: "{{ (system_features_effective.chroot.tool | default('arch-chroot') | string) }}" + system_packages_value: "{{ system.packages if system.packages is defined else [] }}" + system_packages_list_raw: >- + {{ + system_packages_value + if system_packages_value is iterable and system_packages_value is not string + else (system_packages_value | string).split(',') + }} + system_packages_effective: >- + {{ + system_packages_list_raw + | map('trim') + | reject('equalto', '') + | list + }} ansible.builtin.set_fact: hostname: "{{ system_name_effective }}" + os: "{{ system_os_effective | lower }}" + os_version: "{{ system_os_version_effective | string }}" system_cfg: >- {{ system_defaults | combine(system, recursive=True) | combine( { + 'type': system_type_effective | lower, 'name': system_name_effective, + 'os': system_os_effective | lower, + 'os_version': system_os_version_effective | string, 'id': system_id_effective, 'cpus': system_cpus_effective, - 'memory_mb': system_memory_mb_effective, - 'balloon_mb': system_balloon_mb_effective, + 'memory': system_memory_effective, + 'balloon': system_balloon_effective, 'network': system_network_effective, 'vlan': system_vlan_effective, 'ip': system_ip_effective, 'prefix': system_prefix_effective, 'gateway': system_gateway_effective, - 'dns_servers': system_dns_servers_effective, - 'dns_search': system_dns_search_effective, - 'path': system_path_effective + 'dns': { + 'servers': system_dns_resolvers_effective, + 'search': system_dns_domains_effective + }, + 'path': system_path_effective, + 'packages': system_packages_effective, + 'user': { + 'name': system_user_name_effective, + 'password': system_user_password_effective, + 'public_key': system_user_public_key_effective + }, + 'root': { + 'password': system_root_password_effective + }, + 'luks': { + 'enabled': system_luks_enabled_effective, + 'passphrase': system_luks_passphrase_effective, + 'mapper_name': system_luks_mapper_name_effective, + 'auto_decrypt': system_luks_auto_decrypt_effective, + 'auto_decrypt_method': system_luks_auto_decrypt_method_effective, + 'tpm2_device': system_luks_tpm2_device_effective, + 'tpm2_pcrs': system_luks_tpm2_pcrs_effective, + 'keyfile_size': system_luks_keyfile_size_effective, + 'options': system_luks_options_effective, + 'type': system_luks_type_effective, + 'cipher': system_luks_cipher_effective, + 'hash': system_luks_hash_effective, + 'iter_time': system_luks_iter_time_effective, + 'key_size': system_luks_key_size_effective, + 'pbkdf': system_luks_pbkdf_effective, + 'use_urandom': system_luks_use_urandom_effective, + 'verify_passphrase': system_luks_verify_passphrase_effective + }, + 'features': { + 'cis': { + 'enabled': system_features_cis_enabled_effective + }, + 'selinux': { + 'enabled': system_features_selinux_enabled_effective + }, + 'firewall': { + 'enabled': system_features_firewall_enabled_effective, + 'backend': system_features_firewall_backend_effective, + 'toolkit': system_features_firewall_toolkit_effective + }, + 'ssh': { + 'enabled': system_features_ssh_enabled_effective + }, + 'zstd': { + 'enabled': system_features_zstd_enabled_effective + }, + 'swap': { + 'enabled': system_features_swap_enabled_effective + }, + 'banner': { + 'motd': system_features_banner_motd_effective, + 'sudo': system_features_banner_sudo_effective + }, + 'chroot': { + 'tool': system_features_chroot_tool_effective + } + } }, recursive=True ) @@ -123,10 +304,11 @@ system_disk_defaults: size: 0 device: "" - mount: "" - fstype: "" - label: "" - opts: "defaults" + mount: + path: "" + fstype: "" + label: "" + opts: "defaults" system_disks_raw: >- {{ system_cfg.disks @@ -140,13 +322,13 @@ system_disk_device_prefix: >- {{ '/dev/vd' - if (install_type | default('')) == 'virtual' and (hypervisor_type | default('')) == 'libvirt' + if (system_cfg.type | lower) == 'virtual' and (hypervisor_type | default('')) == 'libvirt' else ( '/dev/xvd' - if (install_type | default('')) == 'virtual' and (hypervisor_type | default('')) == 'xen' + if (system_cfg.type | lower) == 'virtual' and (hypervisor_type | default('')) == 'xen' else ( '/dev/sd' - if (install_type | default('')) == 'virtual' + if (system_cfg.type | lower) == 'virtual' and (hypervisor_type | default('')) in ['proxmox', 'vmware'] else '' ) @@ -171,6 +353,16 @@ 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_effective }}" + loop_control: + label: "{{ item | to_json }}" + - name: Validate system disk count ansible.builtin.assert: that: @@ -178,6 +370,11 @@ 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 ansible.builtin.set_fact: system_disks_cfg: "{{ system_disks_cfg | default([]) + [system_disk_cfg] }}" @@ -186,19 +383,12 @@ disk_letter: "{{ system_disk_letter_map[disk_idx] }}" disk_device_default: >- {{ - ( - install_drive - if disk_idx == 0 and install_drive is defined and (install_drive | string | length) > 0 - else (system_disk_device_prefix ~ disk_letter) - ) - if (install_type | default('')) == 'virtual' - else ( - install_drive - if disk_idx == 0 and install_drive is defined and (install_drive | string | length) > 0 - else '' - ) + (system_disk_device_prefix ~ disk_letter) + if (system_cfg.type | lower) == 'virtual' + else '' }} system_disk_cfg_base: "{{ system_disk_defaults | combine(item, recursive=True) }}" + system_disk_mount_path: "{{ (system_disk_cfg_base.mount.path | default('') | string) | trim }}" system_disk_cfg_tmp: >- {{ system_disk_cfg_base @@ -209,14 +399,19 @@ if system_disk_cfg_base.device | string | length > 0 else disk_device_default ), - 'fstype': ( - system_disk_cfg_base.fstype - if system_disk_cfg_base.fstype | string | length > 0 - else ( - 'ext4' - if system_disk_cfg_base.mount | string | length > 0 - else '' - ) + 'mount': ( + system_disk_cfg_base.mount + | combine( + { + 'path': system_disk_mount_path, + 'fstype': ( + system_disk_cfg_base.mount.fstype + if (system_disk_cfg_base.mount.fstype | default('') | string | length) > 0 + else ('ext4' if system_disk_mount_path | length > 0 else '') + ) + }, + recursive=True + ) ) }, recursive=True @@ -248,9 +443,8 @@ system_cfg: "{{ system_cfg | combine({'disks': system_disks_cfg | default([])}, recursive=True) }}" changed_when: false - - name: Set install_drive from system disk definition (if needed) + - name: Set install_drive from system disk definition when: - - (install_drive is not defined) or (install_drive | string | length) == 0 - system_disks_cfg | length > 0 - system_disks_cfg[0].device | string | length > 0 ansible.builtin.set_fact: diff --git a/roles/global_defaults/tasks/validation.yml b/roles/global_defaults/tasks/validation.yml index 3e2a55b..86fea0d 100644 --- a/roles/global_defaults/tasks/validation.yml +++ b/roles/global_defaults/tasks/validation.yml @@ -2,8 +2,10 @@ - name: Validate core variables ansible.builtin.assert: that: - - install_type is defined - - install_type in ["virtual", "physical"] + - system_cfg is defined + - system_cfg is mapping + - system_cfg.type is defined + - system_cfg.type in ["virtual", "physical"] - hypervisor_cfg is defined - hypervisor_cfg is mapping - hypervisor_type is defined @@ -17,11 +19,211 @@ fail_msg: Invalid core variables were specified, please check your inventory/vars. quiet: true -- name: Validate install_type/hypervisor relationship +- name: Validate hypervisor relationship ansible.builtin.assert: that: - - install_type == "physical" or hypervisor_type in ["libvirt", "proxmox", "vmware", "xen"] - fail_msg: "hypervisor must be one of: libvirt, proxmox, vmware, xen when install_type=virtual." + - system_cfg.type == "physical" or hypervisor_type in ["libvirt", "proxmox", "vmware", "xen"] + fail_msg: "hypervisor.type must be one of: libvirt, proxmox, vmware, xen when system.type=virtual." + quiet: true + +- name: Validate hypervisor schema + vars: + hypervisor_allowed_keys: + - type + - url + - username + - password + - host + - storage + - datacenter + - cluster + - validate_certs + - ssh + hypervisor_keys: "{{ (hypervisor | default({})) | dict2items | map(attribute='key') | list }}" + hypervisor_unknown_keys: "{{ hypervisor_keys | difference(hypervisor_allowed_keys) }}" + ansible.builtin.assert: + that: + - hypervisor_unknown_keys | length == 0 + fail_msg: "Unsupported hypervisor keys: {{ hypervisor_unknown_keys | join(', ') }}" + quiet: true + +- name: Validate system schema + vars: + system_allowed_keys: + - type + - os + - os_version + - name + - id + - cpus + - memory + - balloon + - network + - vlan + - ip + - prefix + - gateway + - dns + - path + - packages + - disks + - user + - root + - luks + - features + system_keys: "{{ (system | default({})) | dict2items | map(attribute='key') | list }}" + system_unknown_keys: "{{ system_keys | difference(system_allowed_keys) }}" + ansible.builtin.assert: + that: + - system_unknown_keys | length == 0 + fail_msg: "Unsupported system keys: {{ system_unknown_keys | join(', ') }}" + quiet: true + +- name: Validate nested system schema + vars: + dns_allowed_keys: [servers, search] + user_allowed_keys: [name, password, public_key] + root_allowed_keys: [password] + luks_allowed_keys: + - enabled + - passphrase + - mapper_name + - auto_decrypt + - auto_decrypt_method + - tpm2_device + - tpm2_pcrs + - keyfile_size + - options + - type + - cipher + - hash + - iter_time + - key_size + - pbkdf + - use_urandom + - verify_passphrase + features_allowed_keys: + - cis + - selinux + - firewall + - ssh + - zstd + - swap + - banner + - chroot + feature_leaf_allowed: + cis: [enabled] + selinux: [enabled] + firewall: [enabled, backend, toolkit] + ssh: [enabled] + zstd: [enabled] + swap: [enabled] + banner: [motd, sudo] + chroot: [tool] + dns_keys: "{{ (system.dns | default({})) | dict2items | map(attribute='key') | list }}" + user_keys: "{{ (system.user | default({})) | dict2items | map(attribute='key') | list }}" + root_keys: "{{ (system.root | default({})) | dict2items | map(attribute='key') | list }}" + luks_keys: "{{ (system.luks | default({})) | dict2items | map(attribute='key') | list }}" + features_keys: "{{ (system.features | default({})) | dict2items | map(attribute='key') | list }}" + dns_unknown: "{{ dns_keys | difference(dns_allowed_keys) }}" + user_unknown: "{{ user_keys | difference(user_allowed_keys) }}" + root_unknown: "{{ root_keys | difference(root_allowed_keys) }}" + luks_unknown: "{{ luks_keys | difference(luks_allowed_keys) }}" + features_unknown: "{{ features_keys | difference(features_allowed_keys) }}" + ansible.builtin.assert: + that: + - system.dns is not defined or system.dns is mapping + - system.user is not defined or system.user is mapping + - system.root is not defined or system.root is mapping + - system.luks is not defined or system.luks is mapping + - system.features is not defined or system.features is mapping + - dns_unknown | length == 0 + - user_unknown | length == 0 + - root_unknown | length == 0 + - luks_unknown | length == 0 + - features_unknown | length == 0 + fail_msg: >- + Invalid nested system schema. + dns_unknown={{ dns_unknown | join(',') }}, + user_unknown={{ user_unknown | join(',') }}, + root_unknown={{ root_unknown | join(',') }}, + luks_unknown={{ luks_unknown | join(',') }}, + features_unknown={{ features_unknown | join(',') }} + quiet: true + +- name: Validate feature leaf schemas + vars: + system_features: "{{ system.features | default({}) }}" + feature_keys: "{{ system_features | dict2items | map(attribute='key') | list }}" + feature_leaf_allowed: + cis: [enabled] + selinux: [enabled] + firewall: [enabled, backend, toolkit] + ssh: [enabled] + zstd: [enabled] + swap: [enabled] + banner: [motd, sudo] + chroot: [tool] + ansible.builtin.assert: + that: + - >- + ( + feature_keys + | map('extract', system_features) + | select('mapping') + | list + | length + ) + == (feature_keys | length) + - >- + ( + ( + system_features.cis | default({}) + ) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.cis) + ) | length == 0 + - >- + ( + ( + system_features.selinux | default({}) + ) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.selinux) + ) | length == 0 + - >- + ( + ( + system_features.firewall | default({}) + ) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.firewall) + ) | length == 0 + - >- + ( + ( + system_features.ssh | default({}) + ) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.ssh) + ) | length == 0 + - >- + ( + ( + system_features.zstd | default({}) + ) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.zstd) + ) | length == 0 + - >- + ( + ( + system_features.swap | default({}) + ) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.swap) + ) | length == 0 + - >- + ( + ( + system_features.banner | default({}) + ) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.banner) + ) | length == 0 + - >- + ( + ( + system_features.chroot | default({}) + ) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.chroot) + ) | length == 0 + fail_msg: "Invalid system.features schema detected." quiet: true - name: Validate OS and version inputs @@ -59,23 +261,23 @@ - name: Validate Proxmox hypervisor inputs when: - - install_type == "virtual" + - system_cfg.type == "virtual" - hypervisor_type == "proxmox" ansible.builtin.assert: that: - hypervisor_cfg.url | string | length > 0 - hypervisor_cfg.username | string | length > 0 - hypervisor_cfg.password | string | length > 0 - - hypervisor_cfg.node | string | length > 0 + - hypervisor_cfg.host | string | length > 0 - hypervisor_cfg.storage | string | length > 0 - system_cfg.id | string | length > 0 - system_cfg.network | string | length > 0 - fail_msg: "Missing required Proxmox inputs. Define hypervisor.(url,username,password,node,storage) and system.(id,network)." + fail_msg: "Missing required Proxmox inputs. Define hypervisor.(url,username,password,host,storage) and system.(id,network)." quiet: true - name: Validate VMware hypervisor inputs when: - - install_type == "virtual" + - system_cfg.type == "virtual" - hypervisor_type == "vmware" ansible.builtin.assert: that: @@ -91,7 +293,7 @@ - name: Validate Xen hypervisor inputs when: - - install_type == "virtual" + - system_cfg.type == "virtual" - hypervisor_type == "xen" ansible.builtin.assert: that: @@ -102,40 +304,32 @@ - name: Validate virtual installer ISO requirement ansible.builtin.assert: that: - - install_type == "physical" or (boot_iso is defined and (boot_iso | string | length) > 0) - fail_msg: "boot_iso is required when install_type=virtual." + - system_cfg.type == "physical" or (boot_iso is defined and (boot_iso | string | length) > 0) + fail_msg: "boot_iso is required when system.type=virtual." quiet: true - name: Validate firewall and feature flags ansible.builtin.assert: that: - - firewall_backend is defined - - firewall_backend in ["firewalld", "ufw"] - - firewall_toolkit is defined - - firewall_toolkit in ["iptables", "nftables"] - - firewall_enabled is defined - - motd_enabled is defined - - sudo_banner_enabled is defined - - luks_enabled is defined - - chroot_tool is defined - - chroot_tool in ["arch-chroot", "chroot", "systemd-nspawn"] + - system_cfg.features.firewall.backend is defined + - system_cfg.features.firewall.backend in ["firewalld", "ufw"] + - system_cfg.features.firewall.toolkit is defined + - system_cfg.features.firewall.toolkit in ["iptables", "nftables"] + - system_cfg.features.firewall.enabled is defined + - system_cfg.features.banner.motd is defined + - system_cfg.features.banner.sudo is defined + - system_cfg.luks.enabled is defined + - system_cfg.features.chroot.tool is defined + - system_cfg.features.chroot.tool in ["arch-chroot", "chroot", "systemd-nspawn"] fail_msg: Invalid feature flags were specified, please check your inventory/vars. quiet: true -- name: Validate system configuration exists - ansible.builtin.assert: - that: - - system_cfg is defined - - system_cfg is mapping - fail_msg: "system configuration is missing. Define system: {...}." - quiet: true - - name: Validate virtual system sizing - when: install_type == "virtual" + when: system_cfg.type == "virtual" ansible.builtin.assert: that: - system_cfg.cpus is defined and (system_cfg.cpus | int) > 0 - - system_cfg.memory_mb is defined and (system_cfg.memory_mb | int) > 0 + - system_cfg.memory is defined and (system_cfg.memory | int) > 0 - system_cfg.disks is defined and (system_cfg.disks | length) > 0 - (system_cfg.disks[0].size | float) > 0 - (system_cfg.disks[0].size | float) >= 20 @@ -145,25 +339,25 @@ (system_cfg.disks[0].size | float) >= ( ( - (system_cfg.memory_mb | float / 1024 >= 16.0) + (system_cfg.memory | float / 1024 >= 16.0) | ternary( - (system_cfg.memory_mb | float / 2048), - [system_cfg.memory_mb | float / 1024, 4.0] | max + (system_cfg.memory | float / 2048), + [system_cfg.memory | float / 1024, 4.0] | max ) ) + 5.5 ) ) - fail_msg: "Invalid system sizing. Check system.cpus, system.memory_mb, and system.disks[0].size." + fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size." quiet: true - name: Validate all virtual disks have a positive size - when: install_type == "virtual" + when: system_cfg.type == "virtual" ansible.builtin.assert: that: - item.size is defined - (item.size | float) > 0 - fail_msg: "Each system disk must have a positive size when install_type=virtual: {{ item | to_json }}" + fail_msg: "Each system disk must have a positive size when system.type=virtual: {{ item | to_json }}" quiet: true loop: "{{ system_cfg.disks | default([]) }}" loop_control: @@ -175,8 +369,8 @@ - system_cfg.disks | length > 0 ansible.builtin.assert: that: - - (system_cfg.disks[0].mount | default('') | string | trim) == '' - fail_msg: "system.disks[0].mount must be empty; use system.disks[1:] for additional mounts." + - (system_cfg.disks[0].mount.path | default('') | string | trim) == '' + fail_msg: "system.disks[0].mount.path must be empty; use system.disks[1:] for additional mounts." quiet: true - name: Validate disk mountpoint inputs @@ -185,6 +379,7 @@ {{ (system_cfg.disks | default([])) | map(attribute='mount') + | map(attribute='path') | map('string') | map('trim') | reject('equalto', '') @@ -208,8 +403,8 @@ - /var/cache/pacman/pkg - /var/log - /var/log/audit - disk_mount: "{{ (item.mount | default('') | string) | trim }}" - disk_fstype: "{{ (item.fstype | default('') | string) | trim }}" + disk_mount: "{{ (item.mount.path | default('') | string) | trim }}" + disk_fstype: "{{ (item.mount.fstype | default('') | string) | trim }}" disk_device: "{{ (item.device | default('') | string) | trim }}" disk_size: "{{ item.size | default(0) }}" ansible.builtin.assert: @@ -218,8 +413,8 @@ - disk_mount == "" or disk_mount != "/" - disk_mount == "" or disk_mount not in reserved_mounts - disk_mount == "" or disk_fstype in ["btrfs", "ext4", "xfs"] - - disk_mount == "" or install_type == "virtual" or (disk_device | length) > 0 - - disk_mount == "" or install_type != "virtual" or (disk_size | float) > 0 + - disk_mount == "" or system_cfg.type == "virtual" or (disk_device | length) > 0 + - disk_mount == "" or system_cfg.type != "virtual" or (disk_size | float) > 0 fail_msg: "Invalid system disk entry: {{ item | to_json }}" quiet: true loop: "{{ system_cfg.disks }}"