refactor(global_defaults): single source of truth for family-default resolution

This commit is contained in:
2026-05-28 17:25:23 +02:00
parent 00acd4d200
commit 441876fab9
6 changed files with 49 additions and 70 deletions

View File

@@ -203,6 +203,12 @@ hypervisor_required_fields:
hypervisor: [] hypervisor: []
system: [] system: []
# Family default content mirror URLs, used when content.url is empty.
content_mirror_defaults:
debian: "https://deb.debian.org/debian/"
ubuntu: "http://archive.ubuntu.com/ubuntu/"
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
# Hypervisor-to-disk device prefix mapping for virtual machines. # Hypervisor-to-disk device prefix mapping for virtual machines.
# Physical installs must set system.disks[].device explicitly. # Physical installs must set system.disks[].device explicitly.
hypervisor_disk_device_map: hypervisor_disk_device_map:

View File

@@ -0,0 +1,25 @@
---
# Shared by both the fresh-run path (_normalize_system.yml) and the pre-computed
# enrichment path (system.yml) so the family-default rules live in one place.
- name: Apply family defaults to system_cfg
vars:
_os: "{{ system_cfg.os | default('') | string | lower }}"
ansible.builtin.set_fact:
system_cfg: >-
{{
system_cfg | combine({
'content': {
'source': system_cfg.content.source
if (system_cfg.content.source | default('') | string | trim | length > 0)
else ('dvd' if _os == 'rhel' else 'mirror'),
'url': system_cfg.content.url
if (system_cfg.content.url | default('') | string | trim | length > 0)
else (content_mirror_defaults[_os] | default('')),
},
'features': {'firewall': {'backend':
system_cfg.features.firewall.backend
if (system_cfg.features.firewall.backend | default('') | string | trim | length > 0)
else ('ufw' if _os in os_family_debian else 'firewalld')
}},
}, recursive=True)
}}

View File

@@ -10,29 +10,19 @@
if (system_raw.name | default('') | string | trim | length) > 0 if (system_raw.name | default('') | string | trim | length) > 0
else inventory_hostname else inventory_hostname
}} }}
_mirror_defaults:
debian: "https://deb.debian.org/debian/"
ubuntu: "http://archive.ubuntu.com/ubuntu/"
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
ansible.builtin.set_fact: ansible.builtin.set_fact:
system_cfg: system_cfg:
# --- Identity & platform ---
type: "{{ system_type }}" type: "{{ system_type }}"
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os 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 }}" version: "{{ system_raw.version | default('') | string }}"
filesystem: "{{ system_raw.filesystem | default('') | string | lower }}" filesystem: "{{ system_raw.filesystem | default('') | string | lower }}"
name: "{{ system_name }}" name: "{{ system_name }}"
id: "{{ system_raw.id | default('') | string }}" id: "{{ system_raw.id | default('') | string }}"
# --- VM sizing (ignored for physical) ---
cpus: "{{ [system_raw.cpus | default(0) | int, 0] | max }}" cpus: "{{ [system_raw.cpus | default(0) | int, 0] | max }}"
memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}" memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}"
balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}" balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}"
# --- Network --- # Flat fields and interfaces[] describe the same primary NIC: each is
# Flat fields (bridge, ip, etc.) and interfaces[] express the same primary NIC. # backfilled from the other so consumers reading either form still work.
# When only flat fields are set, a synthetic interfaces[] entry is built below.
# When interfaces[] is set, the flat ip/prefix/gateway are backfilled from
# interfaces[0] so consumers reading the flat fields (e.g. the post-reboot
# reconnect block) still work.
network: network:
bridge: >- bridge: >-
{{ {{
@@ -87,20 +77,13 @@
else [] else []
) )
}} }}
# --- Locale & environment ---
timezone: "{{ system_raw.timezone | string }}" timezone: "{{ system_raw.timezone | string }}"
locale: "{{ system_raw.locale | string }}" locale: "{{ system_raw.locale | string }}"
keymap: "{{ system_raw.keymap | string }}" keymap: "{{ system_raw.keymap | string }}"
content: content:
source: >- # Family defaults for empty source/url are applied by _apply_family_defaults.yml.
{%- set s = system_raw.content.source | default('') | string | lower | trim -%} source: "{{ system_raw.content.source | default('') | string | lower | trim }}"
{%- if s | length > 0 -%}{{ s }} url: "{{ system_raw.content.url | default('') | string | trim }}"
{%- elif (system_raw.os | default('') | string | lower) == 'rhel' -%}dvd
{%- else -%}mirror{%- endif -%}
url: >-
{%- set u = system_raw.content.url | default('') | string | trim -%}
{%- if u | length > 0 -%}{{ u }}
{%- else -%}{{ _mirror_defaults[system_raw.os | default('') | string | lower] | default('') }}{%- endif -%}
proxy: "{{ system_raw.content.proxy | default('') | string | trim }}" proxy: "{{ system_raw.content.proxy | default('') | string | trim }}"
gpgcheck: "{{ system_raw.content.gpgcheck | default(true) | bool }}" gpgcheck: "{{ system_raw.content.gpgcheck | default(true) | bool }}"
satellite: satellite:
@@ -129,13 +112,11 @@
| reject('equalto', '') | reject('equalto', '')
| list | list
}} }}
# --- Storage & accounts ---
disks: "{{ system_raw.disks | default([]) }}" disks: "{{ system_raw.disks | default([]) }}"
users: "{{ system_raw.users | default({}) }}" users: "{{ system_raw.users | default({}) }}"
root: root:
password: "{{ system_raw.root.password | string }}" password: "{{ system_raw.root.password | string }}"
shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}" shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}"
# --- LUKS disk encryption ---
luks: luks:
enabled: "{{ system_raw.luks.enabled | bool }}" enabled: "{{ system_raw.luks.enabled | bool }}"
passphrase: "{{ system_raw.luks.passphrase | string }}" passphrase: "{{ system_raw.luks.passphrase | string }}"
@@ -153,7 +134,6 @@
iter: "{{ system_raw.luks.iter | int }}" iter: "{{ system_raw.luks.iter | int }}"
bits: "{{ system_raw.luks.bits | int }}" bits: "{{ system_raw.luks.bits | int }}"
pbkdf: "{{ system_raw.luks.pbkdf | string }}" pbkdf: "{{ system_raw.luks.pbkdf | string }}"
# --- Feature flags ---
features: features:
cloud_init: "{{ system_raw.features.cloud_init | default(false) | bool }}" cloud_init: "{{ system_raw.features.cloud_init | default(false) | bool }}"
cis: cis:
@@ -165,10 +145,8 @@
enabled: "{{ system_raw.features.selinux.enabled | bool }}" enabled: "{{ system_raw.features.selinux.enabled | bool }}"
firewall: firewall:
enabled: "{{ system_raw.features.firewall.enabled | bool }}" enabled: "{{ system_raw.features.firewall.enabled | bool }}"
backend: >- # Empty backend is family-resolved by _apply_family_defaults.yml.
{{ (system_raw.features.firewall.backend | default('') | string | lower | trim) backend: "{{ system_raw.features.firewall.backend | default('') | string | lower | trim }}"
if (system_raw.features.firewall.backend | default('') | string | lower | trim | length > 0)
else ('ufw' if (system_raw.os | default('') | string | lower) in ['debian', 'ubuntu', 'ubuntu-lts'] else 'firewalld') }}
toolkit: "{{ system_raw.features.firewall.toolkit | string | lower }}" toolkit: "{{ system_raw.features.firewall.toolkit | string | lower }}"
ssh: ssh:
enabled: "{{ system_raw.features.ssh.enabled | bool }}" enabled: "{{ system_raw.features.ssh.enabled | bool }}"
@@ -225,8 +203,7 @@
if (system_raw.features.peripherals.enabled | string | lower) == 'auto' if (system_raw.features.peripherals.enabled | string | lower) == 'auto'
else (system_raw.features.peripherals.enabled | bool) else (system_raw.features.peripherals.enabled | bool)
}} }}
# fingerprint/camera/audio/bluetooth stay tri-state ('auto'|'true'|'false') # Kept tri-state ('auto'|'true'|'false'): 'auto' resolves at install time from detection.
# because the 'auto' branch is resolved at install time using detection results.
fingerprint: >- fingerprint: >-
{{ {{
'auto' 'auto'

View File

@@ -44,7 +44,7 @@
label: "system.features.{{ item }}" label: "system.features.{{ item }}"
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- (system.features[item] | default({})) is mapping - (system_defaults.features[item] is not mapping) or ((system.features[item] | default({})) is mapping)
fail_msg: "system.features.{{ item }} must be a dictionary." fail_msg: "system.features.{{ item }} must be a dictionary."
quiet: true quiet: true

View File

@@ -1,10 +1,7 @@
--- ---
# Two code paths: # Fresh run normalizes raw `system` input. A pre-computed system_cfg (from the main
# 1. Fresh run (system_cfg undefined): normalize from raw `system` input. # project's deploy_iac) is instead merged with system_defaults to fill the fields
# 2. Pre-computed (system_cfg already set, e.g. from main project's deploy_iac): # bootstrap expects, then convenience facts are derived.
# merge with bootstrap system_defaults to fill missing fields (luks, features,
# etc.) that bootstrap expects but the main project doesn't set, then derive
# convenience facts (hostname, os, os_version).
- name: Normalize system and disk configuration - name: Normalize system and disk configuration
when: system_cfg is not defined when: system_cfg is not defined
block: block:
@@ -50,37 +47,6 @@
ansible.builtin.set_fact: ansible.builtin.set_fact:
system_cfg: "{{ system_defaults | combine(system | default({}), recursive=True) | combine(system_cfg, recursive=True) }}" system_cfg: "{{ system_defaults | combine(system | default({}), recursive=True) | combine(system_cfg, recursive=True) }}"
- name: Apply family defaults (content source, firewall backend) for pre-computed system_cfg
when:
- system_cfg is defined
- _bootstrap_needs_enrichment | default(false) | bool
vars:
# Same family resolution as _normalize_system.yml - kept in sync manually.
_mirror_defaults:
debian: "https://deb.debian.org/debian/"
ubuntu: "http://archive.ubuntu.com/ubuntu/"
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
_os: "{{ system_cfg.os | default('') | string | lower }}"
ansible.builtin.set_fact:
system_cfg: >-
{{
system_cfg | combine({
'content': {
'source': system_cfg.content.source
if (system_cfg.content.source | default('') | string | trim | length > 0)
else ('dvd' if _os == 'rhel' else 'mirror'),
'url': system_cfg.content.url
if (system_cfg.content.url | default('') | string | trim | length > 0)
else (_mirror_defaults[_os] | default('')),
},
'features': {'firewall': {'backend':
system_cfg.features.firewall.backend
if (system_cfg.features.firewall.backend | default('') | string | trim | length > 0)
else ('ufw' if _os in ['debian', 'ubuntu', 'ubuntu-lts'] else 'firewalld')
}},
}, recursive=True)
}}
- name: Populate primary network fields from first interface (pre-computed) - name: Populate primary network fields from first interface (pre-computed)
when: when:
- system_cfg is defined - system_cfg is defined
@@ -117,3 +83,8 @@
- system_cfg is defined - system_cfg is defined
- install_drive is not defined - install_drive is not defined
ansible.builtin.include_tasks: _normalize_disks.yml ansible.builtin.include_tasks: _normalize_disks.yml
# Runs on every path before validation, so an empty firewall.backend / content.source
# resolves to the family default even when system_cfg arrived pre-computed.
- name: Apply family defaults (content source, firewall backend)
ansible.builtin.include_tasks: _apply_family_defaults.yml

View File

@@ -96,7 +96,7 @@
quiet: true quiet: true
- name: Validate system.features leaf schemas - name: Validate system.features leaf schemas
loop: "{{ system_defaults.features | dict2items }}" loop: "{{ system_defaults.features | dict2items | selectattr('value', 'mapping') }}"
loop_control: loop_control:
label: "system.features.{{ item.key }}" label: "system.features.{{ item.key }}"
vars: vars: