refactor(schema): simplify dict normalization and schema checks

This commit is contained in:
2026-02-11 05:37:18 +01:00
parent aac2bd0b06
commit 5326907ae9
4 changed files with 207 additions and 269 deletions

View File

@@ -82,3 +82,12 @@ system_defaults:
sudo: true sudo: true
chroot: chroot:
tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn
system_disk_defaults:
size: 0
device: ""
mount:
path: ""
fstype: ""
label: ""
opts: "defaults"

View File

@@ -14,9 +14,7 @@
quiet: true quiet: true
- name: Normalize hypervisor configuration - name: Normalize hypervisor configuration
vars:
hypervisor_cfg_normalized: "{{ hypervisor_defaults | combine(hypervisor, recursive=True) }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
hypervisor_cfg: "{{ hypervisor_cfg_normalized }}" hypervisor_cfg: "{{ hypervisor_defaults | combine(hypervisor, recursive=True) }}"
hypervisor_type: "{{ hypervisor_cfg_normalized.type | string | lower }}" hypervisor_type: "{{ (hypervisor_defaults | combine(hypervisor, recursive=True)).type | string | lower }}"
changed_when: false changed_when: false

View File

@@ -18,70 +18,82 @@
system_os_input: "{{ system_raw.os | default('') | string | lower }}" system_os_input: "{{ system_raw.os | default('') | string | lower }}"
system_name: >- system_name: >-
{{ {{
system_raw.name | string system_raw.name | string | trim
if (system_raw.name | default('') | string | length) > 0 if (system_raw.name | default('') | string | trim | length) > 0
else inventory_hostname else inventory_hostname
}} }}
system_dns: >-
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_features_raw.cis system_defaults.dns
if system_features_raw.cis is defined and system_features_raw.cis is mapping | combine((system_raw.dns if system_raw.dns is mapping else {}), recursive=True)
else {}
}} }}
system_feature_selinux_raw: >- system_user: >-
{{ {{
system_features_raw.selinux system_defaults.user
if system_features_raw.selinux is defined and system_features_raw.selinux is mapping | combine((system_raw.user if system_raw.user is mapping else {}), recursive=True)
else {}
}} }}
system_feature_firewall_raw: >- system_root: >-
{{ {{
system_features_raw.firewall system_defaults.root
if system_features_raw.firewall is defined and system_features_raw.firewall is mapping | combine((system_raw.root if system_raw.root is mapping else {}), recursive=True)
else {}
}} }}
system_feature_ssh_raw: >- system_luks: >-
{{ {{
system_features_raw.ssh system_defaults.luks
if system_features_raw.ssh is defined and system_features_raw.ssh is mapping | combine((system_raw.luks if system_raw.luks is mapping else {}), recursive=True)
else {}
}} }}
system_feature_zstd_raw: >- system_luks_tpm2: >-
{{ {{
system_features_raw.zstd system_defaults.luks.tpm2
if system_features_raw.zstd is defined and system_features_raw.zstd is mapping | combine((system_luks.tpm2 if system_luks.tpm2 is mapping else {}), recursive=True)
else {}
}} }}
system_feature_swap_raw: >- system_features: >-
{{ {{
system_features_raw.swap system_defaults.features
if system_features_raw.swap is defined and system_features_raw.swap is mapping | combine((system_raw.features if system_raw.features is mapping else {}), recursive=True)
else {}
}} }}
system_feature_banner_raw: >- system_feature_cis: >-
{{ {{
system_features_raw.banner system_defaults.features.cis
if system_features_raw.banner is defined and system_features_raw.banner is mapping | combine((system_features.cis if system_features.cis is mapping else {}), recursive=True)
else {}
}} }}
system_feature_chroot_raw: >- system_feature_selinux: >-
{{ {{
system_features_raw.chroot system_defaults.features.selinux
if system_features_raw.chroot is defined and system_features_raw.chroot is mapping | combine((system_features.selinux if system_features.selinux is mapping else {}), recursive=True)
else {}
}} }}
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([]) }}" system_packages_input: "{{ system_raw.packages | default([]) }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
system_cfg: system_cfg:
@@ -140,50 +152,50 @@
}} }}
disks: "{{ system_raw.disks | default([]) }}" disks: "{{ system_raw.disks | default([]) }}"
user: user:
name: "{{ system_user_raw.name | default('') | string }}" name: "{{ system_user.name | string }}"
password: "{{ system_user_raw.password | default('') | string }}" password: "{{ system_user.password | string }}"
key: "{{ system_user_raw.key | default('') | string }}" key: "{{ system_user.key | string }}"
root: root:
password: "{{ system_root_raw.password | default('') | string }}" password: "{{ system_root.password | string }}"
luks: luks:
enabled: "{{ system_luks_raw.enabled | default(system_defaults.luks.enabled) | bool }}" enabled: "{{ system_luks.enabled | bool }}"
passphrase: "{{ system_luks_raw.passphrase | default(system_defaults.luks.passphrase) | string }}" passphrase: "{{ system_luks.passphrase | string }}"
mapper: "{{ system_luks_raw.mapper | default(system_defaults.luks.mapper) | string }}" mapper: "{{ system_luks.mapper | string }}"
auto: "{{ system_luks_raw.auto | default(system_defaults.luks.auto) | bool }}" auto: "{{ system_luks.auto | bool }}"
method: "{{ system_luks_raw.method | default(system_defaults.luks.method) | string | lower }}" method: "{{ system_luks.method | string | lower }}"
tpm2: tpm2:
device: "{{ system_luks_tpm2_raw.device | default(system_defaults.luks.tpm2.device) | string }}" device: "{{ system_luks_tpm2.device | string }}"
pcrs: "{{ system_luks_tpm2_raw.pcrs | default(system_defaults.luks.tpm2.pcrs) | string }}" pcrs: "{{ system_luks_tpm2.pcrs | string }}"
keysize: "{{ system_luks_raw.keysize | default(system_defaults.luks.keysize) | int }}" keysize: "{{ system_luks.keysize | int }}"
options: "{{ system_luks_raw.options | default(system_defaults.luks.options) | string }}" options: "{{ system_luks.options | string }}"
type: "{{ system_luks_raw.type | default(system_defaults.luks.type) | string }}" type: "{{ system_luks.type | string }}"
cipher: "{{ system_luks_raw.cipher | default(system_defaults.luks.cipher) | string }}" cipher: "{{ system_luks.cipher | string }}"
hash: "{{ system_luks_raw.hash | default(system_defaults.luks.hash) | string }}" hash: "{{ system_luks.hash | string }}"
iter: "{{ system_luks_raw.iter | default(system_defaults.luks.iter) | int }}" iter: "{{ system_luks.iter | int }}"
bits: "{{ system_luks_raw.bits | default(system_defaults.luks.bits) | int }}" bits: "{{ system_luks.bits | int }}"
pbkdf: "{{ system_luks_raw.pbkdf | default(system_defaults.luks.pbkdf) | string }}" pbkdf: "{{ system_luks.pbkdf | string }}"
urandom: "{{ system_luks_raw.urandom | default(system_defaults.luks.urandom) | bool }}" urandom: "{{ system_luks.urandom | bool }}"
verify: "{{ system_luks_raw.verify | default(system_defaults.luks.verify) | bool }}" verify: "{{ system_luks.verify | bool }}"
features: features:
cis: cis:
enabled: "{{ system_feature_cis_raw.enabled | default(system_defaults.features.cis.enabled) | bool }}" enabled: "{{ system_feature_cis.enabled | bool }}"
selinux: selinux:
enabled: "{{ system_feature_selinux_raw.enabled | default(system_defaults.features.selinux.enabled) | bool }}" enabled: "{{ system_feature_selinux.enabled | bool }}"
firewall: firewall:
enabled: "{{ system_feature_firewall_raw.enabled | default(system_defaults.features.firewall.enabled) | bool }}" enabled: "{{ system_feature_firewall.enabled | bool }}"
backend: "{{ system_feature_firewall_raw.backend | default(system_defaults.features.firewall.backend) | string | lower }}" backend: "{{ system_feature_firewall.backend | string | lower }}"
toolkit: "{{ system_feature_firewall_raw.toolkit | default(system_defaults.features.firewall.toolkit) | string | lower }}" toolkit: "{{ system_feature_firewall.toolkit | string | lower }}"
ssh: ssh:
enabled: "{{ system_feature_ssh_raw.enabled | default(system_defaults.features.ssh.enabled) | bool }}" enabled: "{{ system_feature_ssh.enabled | bool }}"
zstd: zstd:
enabled: "{{ system_feature_zstd_raw.enabled | default(system_defaults.features.zstd.enabled) | bool }}" enabled: "{{ system_feature_zstd.enabled | bool }}"
swap: swap:
enabled: "{{ system_feature_swap_raw.enabled | default(system_defaults.features.swap.enabled) | bool }}" enabled: "{{ system_feature_swap.enabled | bool }}"
banner: banner:
motd: "{{ system_feature_banner_raw.motd | default(system_defaults.features.banner.motd) | bool }}" motd: "{{ system_feature_banner.motd | bool }}"
sudo: "{{ system_feature_banner_raw.sudo | default(system_defaults.features.banner.sudo) | bool }}" sudo: "{{ system_feature_banner.sudo | bool }}"
chroot: chroot:
tool: "{{ system_feature_chroot_raw.tool | default(system_defaults.features.chroot.tool) | string }}" tool: "{{ system_feature_chroot.tool | string }}"
hostname: "{{ system_name }}" 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 ('archlinux' if system_type == 'physical' else '') }}"
os_version: "{{ system_raw.version | default('') | string }}" os_version: "{{ system_raw.version | default('') | string }}"
@@ -192,14 +204,6 @@
- name: Normalize system disks input - name: Normalize system disks input
vars: vars:
system_disks: "{{ system_cfg.disks | default([]) }}" 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_letter_map: "abcdefghijklmnopqrstuvwxyz"
system_disk_device_prefix: >- system_disk_device_prefix: >-
{{ {{

View File

@@ -28,17 +28,7 @@
- name: Validate hypervisor schema - name: Validate hypervisor schema
vars: vars:
hypervisor_allowed_keys: hypervisor_allowed_keys: "{{ hypervisor_defaults | dict2items | map(attribute='key') | list }}"
- type
- url
- username
- password
- host
- storage
- datacenter
- cluster
- certs
- ssh
hypervisor_keys: "{{ (hypervisor | default({})) | dict2items | map(attribute='key') | list }}" hypervisor_keys: "{{ (hypervisor | default({})) | dict2items | map(attribute='key') | list }}"
hypervisor_unknown_keys: "{{ hypervisor_keys | difference(hypervisor_allowed_keys) }}" hypervisor_unknown_keys: "{{ hypervisor_keys | difference(hypervisor_allowed_keys) }}"
ansible.builtin.assert: ansible.builtin.assert:
@@ -49,192 +39,129 @@
- name: Validate system schema - name: Validate system schema
vars: vars:
system_allowed_keys: system_allowed_keys: "{{ system_defaults | dict2items | map(attribute='key') | list }}"
- type
- 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_keys: "{{ (system | default({})) | dict2items | map(attribute='key') | list }}"
system_unknown_keys: "{{ system_keys | difference(system_allowed_keys) }}" system_unknown_keys: "{{ system_keys | difference(system_allowed_keys) }}"
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- system_unknown_keys | length == 0 - 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 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: vars:
dns_allowed_keys: [servers, search] dns_allowed_keys: "{{ system_defaults.dns | dict2items | map(attribute='key') | list }}"
user_allowed_keys: [name, password, key] dns_unknown: >-
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: >-
{{ {{
( ((system.dns | default({})) | dict2items | map(attribute='key') | list)
(system.luks if (system.luks is defined and system.luks is mapping) else {}).tpm2 | difference(dns_allowed_keys)
| default({})
) | dict2items | map(attribute='key') | list
}} }}
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: ansible.builtin.assert:
that: 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 - dns_unknown | length == 0
- user_unknown | length == 0 fail_msg: "Unsupported system.dns keys: {{ dns_unknown | join(', ') }}"
- 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(',') }}
quiet: true quiet: true
- name: Validate feature leaf schemas - name: Validate system.user schema
vars: vars:
system_features: "{{ system.features | default({}) }}" user_allowed_keys: "{{ system_defaults.user | dict2items | map(attribute='key') | list }}"
feature_keys: "{{ system_features | dict2items | map(attribute='key') | list }}" user_unknown: >-
feature_leaf_allowed: {{
cis: [enabled] ((system.user | default({})) | dict2items | map(attribute='key') | list)
selinux: [enabled] | difference(user_allowed_keys)
firewall: [enabled, backend, toolkit] }}
ssh: [enabled]
zstd: [enabled]
swap: [enabled]
banner: [motd, sudo]
chroot: [tool]
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- >- - user_unknown | length == 0
( fail_msg: "Unsupported system.user keys: {{ user_unknown | join(', ') }}"
feature_keys quiet: true
| map('extract', system_features)
| select('mapping') - name: Validate system.root schema
| list vars:
| length root_allowed_keys: "{{ system_defaults.root | dict2items | map(attribute='key') | list }}"
) root_unknown: >-
== (feature_keys | length) {{
- >- ((system.root | default({})) | dict2items | map(attribute='key') | list)
( | difference(root_allowed_keys)
( }}
system_features.cis | default({}) ansible.builtin.assert:
) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.cis) that:
) | length == 0 - root_unknown | length == 0
- >- fail_msg: "Unsupported system.root keys: {{ root_unknown | join(', ') }}"
( quiet: true
(
system_features.selinux | default({}) - name: Validate system.luks schema
) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.selinux) vars:
) | length == 0 luks_allowed_keys: "{{ system_defaults.luks | dict2items | map(attribute='key') | list }}"
- >- luks_unknown: >-
( {{
( ((system.luks | default({})) | dict2items | map(attribute='key') | list)
system_features.firewall | default({}) | difference(luks_allowed_keys)
) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.firewall) }}
) | length == 0 ansible.builtin.assert:
- >- that:
( - luks_unknown | length == 0
( fail_msg: "Unsupported system.luks keys: {{ luks_unknown | join(', ') }}"
system_features.ssh | default({}) quiet: true
) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.ssh)
) | length == 0 - name: Validate system.luks.tpm2 schema
- >- vars:
( tpm2_input: >-
( {{
system_features.zstd | default({}) (system.luks if (system.luks is defined and system.luks is mapping) else {}).tpm2
) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.zstd) | default({})
) | length == 0 }}
- >- 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:
system_features.swap | default({}) that:
) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.swap) - system.luks is not defined or system.luks.tpm2 is not defined or system.luks.tpm2 is mapping
) | length == 0 - tpm2_unknown | length == 0
- >- fail_msg: "Unsupported system.luks.tpm2 keys: {{ tpm2_unknown | join(', ') }}"
( quiet: true
(
system_features.banner | default({}) - name: Validate system.features schema
) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.banner) vars:
) | length == 0 features_allowed_keys: "{{ system_defaults.features | dict2items | map(attribute='key') | list }}"
- >- features_unknown: >-
( {{
( ((system.features | default({})) | dict2items | map(attribute='key') | list)
system_features.chroot | default({}) | difference(features_allowed_keys)
) | dict2items | map(attribute='key') | list | difference(feature_leaf_allowed.chroot) }}
) | length == 0 ansible.builtin.assert:
fail_msg: "Invalid system.features schema detected." 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 quiet: true
- name: Validate OS and version inputs - name: Validate OS and version inputs