From 5326907ae953e8665e0cbcbf2581b6865bc21911 Mon Sep 17 00:00:00 2001 From: Sandwich Date: Wed, 11 Feb 2026 05:37:18 +0100 Subject: [PATCH] refactor(schema): simplify dict normalization and schema checks --- roles/global_defaults/defaults/main.yml | 9 + roles/global_defaults/tasks/hypervisor.yml | 6 +- roles/global_defaults/tasks/system.yml | 176 ++++++------- roles/global_defaults/tasks/validation.yml | 285 ++++++++------------- 4 files changed, 207 insertions(+), 269 deletions(-) diff --git a/roles/global_defaults/defaults/main.yml b/roles/global_defaults/defaults/main.yml index 1636f47..ff3ac07 100644 --- a/roles/global_defaults/defaults/main.yml +++ b/roles/global_defaults/defaults/main.yml @@ -82,3 +82,12 @@ system_defaults: sudo: true chroot: tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn + +system_disk_defaults: + size: 0 + device: "" + mount: + path: "" + fstype: "" + label: "" + opts: "defaults" diff --git a/roles/global_defaults/tasks/hypervisor.yml b/roles/global_defaults/tasks/hypervisor.yml index 25ab0f7..d1214b2 100644 --- a/roles/global_defaults/tasks/hypervisor.yml +++ b/roles/global_defaults/tasks/hypervisor.yml @@ -14,9 +14,7 @@ quiet: true - name: Normalize hypervisor configuration - vars: - hypervisor_cfg_normalized: "{{ hypervisor_defaults | combine(hypervisor, recursive=True) }}" ansible.builtin.set_fact: - hypervisor_cfg: "{{ hypervisor_cfg_normalized }}" - hypervisor_type: "{{ hypervisor_cfg_normalized.type | string | lower }}" + hypervisor_cfg: "{{ hypervisor_defaults | combine(hypervisor, recursive=True) }}" + hypervisor_type: "{{ (hypervisor_defaults | combine(hypervisor, recursive=True)).type | string | lower }}" changed_when: false diff --git a/roles/global_defaults/tasks/system.yml b/roles/global_defaults/tasks/system.yml index 6ab7d8b..6524aba 100644 --- a/roles/global_defaults/tasks/system.yml +++ b/roles/global_defaults/tasks/system.yml @@ -18,70 +18,82 @@ system_os_input: "{{ system_raw.os | default('') | string | lower }}" system_name: >- {{ - system_raw.name | string - if (system_raw.name | default('') | string | length) > 0 + system_raw.name | string | trim + if (system_raw.name | default('') | string | trim | length) > 0 else inventory_hostname }} - - system_dns_raw: "{{ system_raw.dns if system_raw.dns is mapping else {} }}" - system_dns_servers_input: "{{ system_dns_raw.servers | default([]) }}" - system_dns_search_input: "{{ system_dns_raw.search | default([]) }}" - - system_user_raw: "{{ system_raw.user if system_raw.user is mapping else {} }}" - system_root_raw: "{{ system_raw.root if system_raw.root is mapping else {} }}" - system_luks_raw: "{{ system_raw.luks if system_raw.luks is mapping else {} }}" - system_luks_tpm2_raw: "{{ system_luks_raw.tpm2 if system_luks_raw.tpm2 is mapping else {} }}" - system_features_raw: "{{ system_raw.features if system_raw.features is mapping else {} }}" - - system_feature_cis_raw: >- + system_dns: >- {{ - system_features_raw.cis - if system_features_raw.cis is defined and system_features_raw.cis is mapping - else {} + system_defaults.dns + | combine((system_raw.dns if system_raw.dns is mapping else {}), recursive=True) }} - system_feature_selinux_raw: >- + system_user: >- {{ - system_features_raw.selinux - if system_features_raw.selinux is defined and system_features_raw.selinux is mapping - else {} + system_defaults.user + | combine((system_raw.user if system_raw.user is mapping else {}), recursive=True) }} - system_feature_firewall_raw: >- + system_root: >- {{ - system_features_raw.firewall - if system_features_raw.firewall is defined and system_features_raw.firewall is mapping - else {} + system_defaults.root + | combine((system_raw.root if system_raw.root is mapping else {}), recursive=True) }} - system_feature_ssh_raw: >- + system_luks: >- {{ - system_features_raw.ssh - if system_features_raw.ssh is defined and system_features_raw.ssh is mapping - else {} + system_defaults.luks + | combine((system_raw.luks if system_raw.luks is mapping else {}), recursive=True) }} - system_feature_zstd_raw: >- + system_luks_tpm2: >- {{ - system_features_raw.zstd - if system_features_raw.zstd is defined and system_features_raw.zstd is mapping - else {} + system_defaults.luks.tpm2 + | combine((system_luks.tpm2 if system_luks.tpm2 is mapping else {}), recursive=True) }} - system_feature_swap_raw: >- + system_features: >- {{ - system_features_raw.swap - if system_features_raw.swap is defined and system_features_raw.swap is mapping - else {} + system_defaults.features + | combine((system_raw.features if system_raw.features is mapping else {}), recursive=True) }} - system_feature_banner_raw: >- + system_feature_cis: >- {{ - system_features_raw.banner - if system_features_raw.banner is defined and system_features_raw.banner is mapping - else {} + system_defaults.features.cis + | combine((system_features.cis if system_features.cis is mapping else {}), recursive=True) }} - system_feature_chroot_raw: >- + system_feature_selinux: >- {{ - system_features_raw.chroot - if system_features_raw.chroot is defined and system_features_raw.chroot is mapping - else {} + 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: @@ -140,50 +152,50 @@ }} disks: "{{ system_raw.disks | default([]) }}" user: - name: "{{ system_user_raw.name | default('') | string }}" - password: "{{ system_user_raw.password | default('') | string }}" - key: "{{ system_user_raw.key | default('') | string }}" + name: "{{ system_user.name | string }}" + password: "{{ system_user.password | string }}" + key: "{{ system_user.key | string }}" root: - password: "{{ system_root_raw.password | default('') | string }}" + password: "{{ system_root.password | string }}" luks: - enabled: "{{ system_luks_raw.enabled | default(system_defaults.luks.enabled) | bool }}" - passphrase: "{{ system_luks_raw.passphrase | default(system_defaults.luks.passphrase) | string }}" - mapper: "{{ system_luks_raw.mapper | default(system_defaults.luks.mapper) | string }}" - auto: "{{ system_luks_raw.auto | default(system_defaults.luks.auto) | bool }}" - method: "{{ system_luks_raw.method | default(system_defaults.luks.method) | string | lower }}" + 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_raw.device | default(system_defaults.luks.tpm2.device) | string }}" - pcrs: "{{ system_luks_tpm2_raw.pcrs | default(system_defaults.luks.tpm2.pcrs) | string }}" - keysize: "{{ system_luks_raw.keysize | default(system_defaults.luks.keysize) | int }}" - options: "{{ system_luks_raw.options | default(system_defaults.luks.options) | string }}" - type: "{{ system_luks_raw.type | default(system_defaults.luks.type) | string }}" - cipher: "{{ system_luks_raw.cipher | default(system_defaults.luks.cipher) | string }}" - hash: "{{ system_luks_raw.hash | default(system_defaults.luks.hash) | string }}" - iter: "{{ system_luks_raw.iter | default(system_defaults.luks.iter) | int }}" - bits: "{{ system_luks_raw.bits | default(system_defaults.luks.bits) | int }}" - pbkdf: "{{ system_luks_raw.pbkdf | default(system_defaults.luks.pbkdf) | string }}" - urandom: "{{ system_luks_raw.urandom | default(system_defaults.luks.urandom) | bool }}" - verify: "{{ system_luks_raw.verify | default(system_defaults.luks.verify) | bool }}" + 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_raw.enabled | default(system_defaults.features.cis.enabled) | bool }}" + enabled: "{{ system_feature_cis.enabled | bool }}" selinux: - enabled: "{{ system_feature_selinux_raw.enabled | default(system_defaults.features.selinux.enabled) | bool }}" + enabled: "{{ system_feature_selinux.enabled | bool }}" firewall: - enabled: "{{ system_feature_firewall_raw.enabled | default(system_defaults.features.firewall.enabled) | bool }}" - backend: "{{ system_feature_firewall_raw.backend | default(system_defaults.features.firewall.backend) | string | lower }}" - toolkit: "{{ system_feature_firewall_raw.toolkit | default(system_defaults.features.firewall.toolkit) | string | lower }}" + 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_raw.enabled | default(system_defaults.features.ssh.enabled) | bool }}" + enabled: "{{ system_feature_ssh.enabled | bool }}" zstd: - enabled: "{{ system_feature_zstd_raw.enabled | default(system_defaults.features.zstd.enabled) | bool }}" + enabled: "{{ system_feature_zstd.enabled | bool }}" swap: - enabled: "{{ system_feature_swap_raw.enabled | default(system_defaults.features.swap.enabled) | bool }}" + enabled: "{{ system_feature_swap.enabled | bool }}" banner: - motd: "{{ system_feature_banner_raw.motd | default(system_defaults.features.banner.motd) | bool }}" - sudo: "{{ system_feature_banner_raw.sudo | default(system_defaults.features.banner.sudo) | bool }}" + motd: "{{ system_feature_banner.motd | bool }}" + sudo: "{{ system_feature_banner.sudo | bool }}" chroot: - tool: "{{ system_feature_chroot_raw.tool | default(system_defaults.features.chroot.tool) | string }}" + 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 }}" @@ -192,14 +204,6 @@ - name: Normalize system disks input vars: system_disks: "{{ system_cfg.disks | default([]) }}" - system_disk_defaults: - size: 0 - device: "" - mount: - path: "" - fstype: "" - label: "" - opts: "defaults" system_disk_letter_map: "abcdefghijklmnopqrstuvwxyz" system_disk_device_prefix: >- {{ diff --git a/roles/global_defaults/tasks/validation.yml b/roles/global_defaults/tasks/validation.yml index 1d5a3f4..ddf54b1 100644 --- a/roles/global_defaults/tasks/validation.yml +++ b/roles/global_defaults/tasks/validation.yml @@ -28,17 +28,7 @@ - name: Validate hypervisor schema vars: - hypervisor_allowed_keys: - - type - - url - - username - - password - - host - - storage - - datacenter - - cluster - - certs - - ssh + hypervisor_allowed_keys: "{{ hypervisor_defaults | dict2items | map(attribute='key') | list }}" hypervisor_keys: "{{ (hypervisor | default({})) | dict2items | map(attribute='key') | list }}" hypervisor_unknown_keys: "{{ hypervisor_keys | difference(hypervisor_allowed_keys) }}" ansible.builtin.assert: @@ -49,192 +39,129 @@ - name: Validate system schema vars: - system_allowed_keys: - - type - - os - - version - - name - - id - - cpus - - memory - - balloon - - network - - vlan - - ip - - prefix - - gateway - - dns - - path - - packages - - disks - - user - - root - - luks - - features + system_allowed_keys: "{{ system_defaults | dict2items | map(attribute='key') | list }}" 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(', ') }}" + fail_msg: "Unsupported system keys: {{ system_unknown_keys | join(', ') }}." quiet: true -- name: Validate nested system schema +- name: Validate nested system mappings + loop: + - dns + - user + - root + - luks + - features + loop_control: + label: "{{ item }}" + ansible.builtin.assert: + that: + - system[item] is not defined or system[item] is mapping + fail_msg: "system.{{ item }} must be a dictionary." + quiet: true + +- name: Validate system.dns schema vars: - dns_allowed_keys: [servers, search] - user_allowed_keys: [name, password, key] - root_allowed_keys: [password] - luks_allowed_keys: - - enabled - - passphrase - - mapper - - auto - - method - - tpm2 - - keysize - - options - - type - - cipher - - hash - - iter - - bits - - pbkdf - - urandom - - verify - 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 }}" - tpm2_keys: >- + dns_allowed_keys: "{{ system_defaults.dns | dict2items | map(attribute='key') | list }}" + dns_unknown: >- {{ - ( - (system.luks if (system.luks is defined and system.luks is mapping) else {}).tpm2 - | default({}) - ) | dict2items | map(attribute='key') | list + ((system.dns | default({})) | dict2items | map(attribute='key') | list) + | difference(dns_allowed_keys) }} - tpm2_allowed_keys: [device, pcrs] - 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) }}" - tpm2_unknown: "{{ tpm2_keys | difference(tpm2_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.luks is not defined or system.luks.tpm2 is not defined or system.luks.tpm2 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 - - tpm2_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(',') }}, - tpm2_unknown={{ tpm2_unknown | join(',') }}, - features_unknown={{ features_unknown | join(',') }} + fail_msg: "Unsupported system.dns keys: {{ dns_unknown | join(', ') }}" quiet: true -- name: Validate feature leaf schemas +- name: Validate system.user schema 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] + user_allowed_keys: "{{ system_defaults.user | dict2items | map(attribute='key') | list }}" + user_unknown: >- + {{ + ((system.user | default({})) | dict2items | map(attribute='key') | list) + | difference(user_allowed_keys) + }} 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." + - user_unknown | length == 0 + fail_msg: "Unsupported system.user keys: {{ user_unknown | join(', ') }}" + quiet: true + +- name: Validate system.root schema + vars: + root_allowed_keys: "{{ system_defaults.root | dict2items | map(attribute='key') | list }}" + root_unknown: >- + {{ + ((system.root | default({})) | dict2items | map(attribute='key') | list) + | difference(root_allowed_keys) + }} + ansible.builtin.assert: + that: + - root_unknown | length == 0 + fail_msg: "Unsupported system.root keys: {{ root_unknown | join(', ') }}" + quiet: true + +- name: Validate system.luks schema + vars: + luks_allowed_keys: "{{ system_defaults.luks | dict2items | map(attribute='key') | list }}" + luks_unknown: >- + {{ + ((system.luks | default({})) | dict2items | map(attribute='key') | list) + | difference(luks_allowed_keys) + }} + ansible.builtin.assert: + that: + - luks_unknown | length == 0 + fail_msg: "Unsupported system.luks keys: {{ luks_unknown | join(', ') }}" + quiet: true + +- name: Validate system.luks.tpm2 schema + vars: + tpm2_input: >- + {{ + (system.luks if (system.luks is defined and system.luks is mapping) else {}).tpm2 + | default({}) + }} + tpm2_allowed_keys: "{{ system_defaults.luks.tpm2 | dict2items | map(attribute='key') | list }}" + tpm2_unknown: "{{ (tpm2_input | dict2items | map(attribute='key') | list) | difference(tpm2_allowed_keys) }}" + ansible.builtin.assert: + that: + - system.luks is not defined or system.luks.tpm2 is not defined or system.luks.tpm2 is mapping + - tpm2_unknown | length == 0 + fail_msg: "Unsupported system.luks.tpm2 keys: {{ tpm2_unknown | join(', ') }}" + quiet: true + +- name: Validate system.features schema + vars: + features_allowed_keys: "{{ system_defaults.features | dict2items | map(attribute='key') | list }}" + features_unknown: >- + {{ + ((system.features | default({})) | dict2items | map(attribute='key') | list) + | difference(features_allowed_keys) + }} + ansible.builtin.assert: + that: + - features_unknown | length == 0 + fail_msg: "Unsupported system.features keys: {{ features_unknown | join(', ') }}" + quiet: true + +- name: Validate system.features leaf schemas + loop: "{{ system_defaults.features | dict2items }}" + loop_control: + label: "system.features.{{ item.key }}" + vars: + feature_input: "{{ (system.features | default({}))[item.key] | default({}) }}" + feature_allowed_keys: "{{ item.value | dict2items | map(attribute='key') | list }}" + feature_unknown: "{{ (feature_input | dict2items | map(attribute='key') | list) | difference(feature_allowed_keys) }}" + ansible.builtin.assert: + that: + - feature_input is mapping + - feature_unknown | length == 0 + fail_msg: "Unsupported system.features.{{ item.key }} keys: {{ feature_unknown | join(', ') }}" quiet: true - name: Validate OS and version inputs