Compare commits

..
86 Commits
Author SHA1 Message Date
sandwich 43755a6016 fix(partitioning): resolve vmware disks by scsi target instead of the sdX letter 2026-09-15 20:19:53 +02:00
sandwich a795535305 fix(configuration): disable selinux with selinux=0 on el9 when the feature is off 2026-08-31 13:45:06 +02:00
sandwich a9a9706f2c fix(configuration): disable the preset-enabled firewall daemon when the feature is off 2026-08-31 13:40:41 +02:00
sandwich f5bb5e92be fix(system_check): retry transient vcenter api errors in the preflight lookups 2026-08-31 13:11:16 +02:00
sandwich 22e66959ee fix(global_defaults): normalize disks when install_drive is supplied externally 2026-08-24 18:38:48 +02:00
sandwich 332dfeb6f1 fix(configuration): put new EFI boot entry first and install removable fallback 2026-07-31 20:33:54 +02:00
sandwich 19029bdc8b fix(bootstrap): map Intel firmware to linux-firmware-intel so iwlwifi loads 2026-07-31 20:33:54 +02:00
sandwich 9c4a86c0ec style(cleanup): drop redundant comments from the power-off tasks 2026-07-15 00:09:44 +02:00
sandwich 723f0516da fix(cleanup): wait for the guest shutdown before forcing the VM off 2026-07-15 00:05:38 +02:00
sandwich 975384661e docs(content): correct the family default to rhel=dvd, not EL=dvd 2026-07-14 21:14:41 +02:00
sandwich ad25bec9fd fix(cleanup): force-stop the vmware VM before booting the installed OS 2026-07-14 06:48:25 +02:00
sandwich 6240217318 feat(system_check): preflight boot media and datastore placement 2026-07-14 01:48:26 +02:00
sandwich 1c00ec9370 fix(configuration): grant the platform sudo group only when sudo is set 2026-07-14 01:48:26 +02:00
sandwich 371d5a0acd fix(configuration): deploy sudoers rules for bool-or-string sudo values 2026-07-14 01:48:26 +02:00
sandwich 41ccf2a5b9 fix(configuration): allow guest-exec RPCs for EL golden cloning 2026-05-29 22:43:14 +02:00
sandwich 8f9cfe3b2f fix(cleanup): force-stop then start the proxmox VM after install 2026-05-29 22:40:02 +02:00
sandwich 6a75237197 fix(environment): pick the largest cdrom as the RHEL install DVD 2026-05-28 17:52:44 +02:00
sandwich b04aad12fb fix(satellite): drop auto-attach (mutually exclusive with activation key) 2026-05-26 01:08:09 +02:00
sandwich 4fff9f8d80 fix(virtualization): create vmware target folder before vm deploy 2026-06-04 18:17:50 +02:00
sandwich 7f12a0f3d8 feat: accept proxmox API-token auth alongside password 2026-05-31 12:40:31 +02:00
sandwich ceb2237bbb fix(encryption): add tpm2-tss dracut module explicitly for TPM2 LUKS 2026-05-31 12:39:24 +02:00
sandwich 477c8379c4 fix(configuration): enable per-family time-sync and skip sudo-rs lecture 2026-05-31 12:30:26 +02:00
sandwich 579c499c02 feat(configuration): multi-backend networking, bind by match not MAC 2026-05-31 12:25:53 +02:00
sandwich 89e366d0f0 fix: EL10 PAM and crypto readiness via authselect profile and DEFAULT policy 2026-05-28 17:30:57 +02:00
sandwich 6fe843355e fix(bootstrap): keep package cache off the 2 GiB CIS /var during install 2026-05-28 17:26:25 +02:00
sandwich 441876fab9 refactor(global_defaults): single source of truth for family-default resolution 2026-05-28 17:25:23 +02:00
sandwich 00acd4d200 refactor(configuration): consolidate firewall into one phase-aware path 2026-05-27 05:28:00 +02:00
sandwich d922efd2e4 feat: uniform system.content source schema across installers and repositories 2026-05-27 05:15:32 +02:00
sandwich 939c5c741f feat: golden-image build support (cloud-init on EL, selinux relabel, SSH wait) 2026-05-27 05:05:55 +02:00
sandwich 2c35409519 feat(cis): add selectable profile and per-rule hardening toggles 2026-05-25 04:37:33 +02:00
sandwich d2a19cfd5c feat(hardware): auto-detect audio, bluetooth, camera with declarative override 2026-05-25 04:36:21 +02:00
sandwich 44f5adc682 feat(bootstrap): per-os desktop apps, KDE plasma-login-manager and DM resolution 2026-05-25 04:30:53 +02:00
sandwich 0185797af9 fix(environment): co-upgrade soname closure when installing installer tools 2026-05-25 03:54:12 +02:00
sandwich e0ecf628cd fix(bootstrap): deploy all non-EOL core distros (keyrings, repos, versions) 2026-05-25 03:52:44 +02:00
sandwich 37df881daa docs: refresh bootstrap examples and README 2026-05-30 18:05:14 +02:00
sandwich 55b21eae5d fix: encryption, partitioning, cis and virtualization hardening 2026-05-30 18:05:14 +02:00
sandwich b1e938b7f0 fix(users): accept plaintext or pre-hashed passwords uniformly 2026-05-30 18:05:05 +02:00
sandwich c843f5289b feat: hardware/firmware/gpu/peripherals detection and packages 2026-05-30 18:05:05 +02:00
sandwich 9757ed3785 feat: complete wayland desktop deployment (gnome/kde/sway/hyprland) 2026-05-30 18:05:05 +02:00
sandwich 876e90ce2b refactor: trim bootstrap OS support to core three distro families 2026-05-30 18:04:00 +02:00
sandwich 7c44cb1ff0 docs(bootstrap): fix users dict format in examples, sync schema defaults, document secure_boot/rhel_repo 2026-05-30 09:25:34 +02:00
sandwich 5d0630a386 refactor(global_defaults): drop orphan luks.urandom/verify and aur feature, bump fedora to 45 2026-05-30 09:25:34 +02:00
sandwich 3eaf918a53 fix(lint): convert sshd restart to handler, add pipefail to btrfs subvol set 2026-05-30 09:25:34 +02:00
sandwich 382e82ff85 fix(configuration): tolerate missing units, gate Secure Boot to supported OSes, fix clevis install per family 2026-05-30 09:25:34 +02:00
sandwich db7dc53bd7 docs(bootstrap): document firmware/gpu/peripherals/hardware features 2026-05-30 09:25:34 +02:00
sandwich 7d45f25a7e feat(bootstrap): install vendor-matched hardware packages 2026-05-30 09:25:34 +02:00
sandwich 3880b8f41e feat(environment): detect cpu/gpu/wireless/fingerprint hardware 2026-05-30 09:25:34 +02:00
sandwich dc3c4a901f feat(global_defaults): firmware/gpu/peripherals/hardware schema 2026-05-30 09:25:34 +02:00
sandwich 6bfaa0aa2b fix(configuration): guard user keys access to avoid dict.keys() method collision 2026-04-16 15:37:13 +02:00
sandwich 5dc0424acb feat(cleanup): enroll Secure Boot keys in VM NVRAM after OS installation 2026-04-16 15:37:13 +02:00
sandwich c12332a853 feat(virtualization): enable TPM2 emulation for Secure Boot VMs 2026-04-16 15:37:13 +02:00
sandwich 5ace4577a2 feat(configuration): auto-bind PCR 7 when Secure Boot and FDE are both enabled 2026-04-16 15:37:13 +02:00
sandwich c0ff01ef40 feat(configuration): add Secure Boot tasks for shim and sbctl 2026-04-16 15:37:13 +02:00
sandwich 57417514e3 feat(configuration): override EFI loader to shim when Secure Boot enabled 2026-04-02 04:34:47 +02:00
sandwich 0928588c1f feat(bootstrap): add Secure Boot conditional packages for Debian, Ubuntu, and Arch 2026-04-02 04:34:16 +02:00
sandwich 6d622f2db4 feat(global_defaults): add secure_boot feature toggle with normalization 2026-04-02 04:33:07 +02:00
sandwich b11d65a6f3 docs(bootstrap): document desktop, initramfs, and FDE features with SSH keepalive config 2026-04-01 15:07:58 +02:00
sandwich 3623fc292c feat(configuration): generic FDE with systemd-cryptenroll, clevis fallback, and configurable initramfs 2026-04-01 15:07:58 +02:00
sandwich dfca7ec94b fix(configuration): RedHat EFI grub wrapper with btrfs subvol prefix and boot order 2026-04-01 15:07:58 +02:00
sandwich e8be84bf49 fix(partitioning): set btrfs default subvolume and restrict @pkg to Arch 2026-04-01 15:07:58 +02:00
sandwich 322cc0b1ce fix(bootstrap): resolve interface-only network, sshd penalties, dnf scriptlets, and EFI cleanup 2026-04-01 15:07:58 +02:00
sandwich 4b38754f8b feat(bootstrap): add desktop environment support with configurable DE, DM, and display target 2026-04-01 15:07:58 +02:00
sandwich a6bc7ffe04 fix(configuration): use /boot/grub2/grub.cfg for RedHat EFI grub config 2026-03-25 16:03:39 +01:00
sandwich c529e71ebc feat(packages): add needrestart to Debian and Ubuntu package lists 2026-03-20 18:06:14 +01:00
sandwich cb46de2b6d feat(bootstrap): add full package upgrade step for Debian and Ubuntu 2026-03-20 18:05:04 +01:00
sandwich 9169117b25 fix(vim): use vimscript comment syntax for blockinfile markers in vimrc 2026-03-20 18:00:12 +01:00
sandwich 6c94c519fb fix(sudo): use explicit string check instead of bool conditional for sudo field 2026-03-20 17:31:49 +01:00
sandwich efd96a42b8 fix(connection): set ansible_port explicitly at every connection transition 2026-03-20 17:31:49 +01:00
sandwich 68661c3cca fix(vmware): use primary ansible_* vars for vmware_tools connection plugin precedence 2026-03-20 17:31:49 +01:00
sandwich 1db20c7ac0 fix(vmware): use empty password for vmware_tools during live ISO bootstrap 2026-03-20 17:31:49 +01:00
sandwich 7b155b427b fix(users): update cloud-init template and input validation for dict users 2026-03-20 17:31:49 +01:00
sandwich ca8721e98f refactor(prompts): remove vars_prompt, require users defined in inventory 2026-03-20 17:31:49 +01:00
sandwich cdb2559d8f fix(prompts): add default values to vars_prompt to skip in non-interactive mode 2026-03-20 17:31:49 +01:00
sandwich 443f6623df refactor(users): change system.users from list to dict keyed by username 2026-03-20 17:31:49 +01:00
sandwich 6cf418fe00 fix(configuration): make root password, user keys, and sudo all optional 2026-03-20 17:31:49 +01:00
sandwich 47ec5fe621 fix(cloud-init): handle missing keys and make sudo conditional 2026-03-20 17:31:49 +01:00
sandwich 240f945cce fix(cleanup): remove ansible_become override that blocks swapoff/umount 2026-03-20 17:31:49 +01:00
sandwich 663a04556f feat(global_defaults): add system.features.aur schema for validation passthrough 2026-03-20 17:31:49 +01:00
sandwich 6febd1acf1 refactor(virtualization): extract shared Xen disk definitions 2026-03-12 12:27:18 +01:00
sandwich 008187860c refactor: remove unnecessary changed_when from set_fact tasks 2026-03-12 12:25:45 +01:00
sandwich cd1be6b5e1 refactor(partitioning): remove redundant blockdev --rereadpt calls 2026-03-12 12:25:15 +01:00
sandwich 15be6149fd refactor(partitioning): remove unused register variables 2026-03-12 12:24:59 +01:00
sandwich ca29ad200d chore: suppress args[module] false positives from variable-based module_defaults 2026-03-12 12:12:27 +01:00
sandwich 8079099cee fix(cleanup): add no_log to Proxmox VM restart task 2026-03-12 12:12:27 +01:00
sandwich 9e79185b07 fix(virtualization): add missing changed_when to Xen VM stop task 2026-03-12 12:12:27 +01:00
sandwich b88bf2860f fix(configuration): replace fail+ignore_errors with debug for TPM2 fallback warning 2026-03-12 12:12:27 +01:00
29 changed files with 184 additions and 999 deletions

No files matched your search

-3
View File
@@ -9,6 +9,3 @@ vars_libvirt.yml
vars_proxmox.yml vars_proxmox.yml
.sisyphus/ .sisyphus/
.ansible/
__pycache__/
-3
View File
@@ -1,9 +1,6 @@
--- ---
extends: default extends: default
ignore:
- .ansible/
rules: rules:
document-start: disable document-start: disable
line-length: line-length:
+2 -8
View File
@@ -213,11 +213,11 @@ Uniform package content source, family-resolved. `source: ''` defaults to `dvd`
| `dns.search` | list | `[]` | Search domains (must be a YAML list) | | `dns.search` | list | `[]` | Search domains (must be a YAML list) |
| `interfaces` | list | `[]` | Multi-NIC config (overrides flat fields above) | | `interfaces` | list | `[]` | Multi-NIC config (overrides flat fields above) |
When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`, `vlan`) are auto-wrapped into a single-entry list. When `interfaces` is set, it takes precedence. Each entry supports: `name`, `bridge` (required), `vlan`, `ip`, `prefix`, `gateway`, and `extra_addresses`: further IPv4 CIDRs (`192.0.2.10/24`) on the same NIC beside `ip`, written by every network backend. Proxmox `ipconfigN` carries one address per NIC, so on Proxmox the extras arrive with the installed network config, not cloud-init. When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`, `vlan`) are auto-wrapped into a single-entry list. When `interfaces` is set, it takes precedence. Each entry supports: `name`, `bridge` (required), `vlan`, `ip`, `prefix`, `gateway`.
#### `system.users` #### `system.users`
Dict keyed by username. At least one user must have a non-empty `password`: the first such user, in declaration order, is the account bootstrap logs in as after the reboot and the cloud-init and Proxmox `ciuser` primary user. Users without a password get locked accounts (key-only auth). Dict keyed by username. At least one user must have a `password` (used for SSH access during bootstrap). Users without a password get locked accounts (key-only auth).
```yaml ```yaml
system: system:
@@ -238,11 +238,6 @@ system:
| `password` | string | -- | User password (required for at least one user) | | `password` | string | -- | User password (required for at least one user) |
| `keys` | list | `[]` | SSH public keys | | `keys` | list | `[]` | SSH public keys |
| `sudo` | bool/string | -- | `true` for NOPASSWD ALL, or custom sudoers string | | `sudo` | bool/string | -- | `true` for NOPASSWD ALL, or custom sudoers string |
| `groups` | list | derived | Supplementary groups; empty means the platform sudo group when `sudo` is set |
| `shell` | string | `/bin/bash` | Login shell |
| `password_login` | bool | `false` | Read by the main repository's SSH hardening, ignored here |
| `uid` | int | -- | Read by the main repository's user actions; the bootstrap assigns `1000 + index` |
| `disabled` | bool | `false` | Read by the main repository's user actions, ignored here |
Users must be defined in inventory. The dict format enables additive merging across inventory layers with `hash_behaviour=merge`. Users must be defined in inventory. The dict format enables additive merging across inventory layers with `hash_behaviour=merge`.
@@ -549,7 +544,6 @@ On VMware, `device` is resolved on the target from the SCSI address instead: ent
| ------------- | ------ | ------------------------------------------------------ | | ------------- | ------ | ------------------------------------------------------ |
| `size` | number | Disk size in GB (required for virtual) | | `size` | number | Disk size in GB (required for virtual) |
| `device` | string | Block device path (required for physical data disks) | | `device` | string | Block device path (required for physical data disks) |
| `storage` | string | Proxmox storage for this disk; empty uses `hypervisor.storage` |
| `partition` | string | Derived from `device` during normalization (not user input) | | `partition` | string | Derived from `device` during normalization (not user input) |
| `mount.path` | string | Mount point (additional disks only) | | `mount.path` | string | Mount point (additional disks only) |
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` | | `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
-2
View File
@@ -1,7 +1,5 @@
[defaults] [defaults]
hash_behaviour = merge hash_behaviour = merge
roles_path = roles
action_plugins = plugins/action
interpreter_python = auto_silent interpreter_python = auto_silent
deprecation_warnings = False deprecation_warnings = False
host_key_checking = False host_key_checking = False
+5 -3
View File
@@ -119,13 +119,15 @@
when: when:
- post_reboot_can_connect | bool - post_reboot_can_connect | bool
no_log: true no_log: true
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
ansible_connection: ssh ansible_connection: ssh
ansible_host: "{{ system_cfg.network.ip }}" ansible_host: "{{ system_cfg.network.ip }}"
ansible_port: 22 ansible_port: 22
ansible_user: "{{ bootstrap_primary_user.name }}" ansible_user: "{{ _primary.key }}"
ansible_password: "{{ bootstrap_primary_user.password }}" ansible_password: "{{ _primary.value.password }}"
ansible_become_password: "{{ bootstrap_primary_user.password }}" ansible_become_password: "{{ _primary.value.password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
ansible_python_interpreter: /usr/bin/python3 ansible_python_interpreter: /usr/bin/python3
-152
View File
@@ -1,152 +0,0 @@
"""Strict validation of user-facing variable dicts against role argument specs.
Each spec file's ``argument_specs.inputs.options`` declares top-level variables. Every one the host
defines is checked without Ansible's lenient coercion: a string is never a list, bool or dict. A dict
option whose only sub-option is a placeholder such as ``<name>`` is a collection keyed by name; each
entry is checked against that sub-option, and a null entry means the entry is not managed. A ``raw``
option that declares ``options`` is a role toggle: ``true``, ``false``, or the dict those options
describe. Messages name the key path
and the types involved but never a value, so secrets cannot leak into job output.
"""
import glob
from collections.abc import Mapping
from ansible.errors import AnsibleError
from ansible.parsing.vault import EncryptedString
from ansible.plugins.action import ActionBase
SPEC_KEYS = {"type", "required", "default", "choices", "elements", "options", "description", "no_log"}
UNRESOLVED = object()
# Numbers pass where a string is declared and digit strings where an int is: the roles read those
# through `| string` and `| int` unchanged. An empty string is the contract's unset int, as in
# `system.vlan: ""`. A string posing as a list, bool or dict never passes.
TYPES = {
"str": lambda v: isinstance(v, (str, int, float, EncryptedString)) and not isinstance(v, bool),
"int": lambda v: (isinstance(v, int) and not isinstance(v, bool)) or (isinstance(v, str) and (v == "" or v.isdigit())),
"float": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
"bool": lambda v: isinstance(v, bool),
"list": lambda v: isinstance(v, list),
"dict": lambda v: isinstance(v, Mapping),
"raw": lambda _: True,
}
def _kind(value):
if value is None:
return "null"
# bool before int: bool is an int subclass.
for name, cls in (("bool", bool), ("dict", Mapping), ("str", (str, EncryptedString)), ("int", int),
("float", float), ("list", list)):
if isinstance(value, cls):
return name
return type(value).__name__
def _check_spec(options, path):
for key, spec in options.items():
unknown = set(spec) - SPEC_KEYS
if unknown:
raise AnsibleError(f"{path}.{key}: unsupported spec attributes {', '.join(sorted(unknown))}")
if spec.get("type", "str") not in TYPES:
raise AnsibleError(f"{path}.{key}: unsupported spec type {spec['type']}")
if "options" in spec:
_check_spec(spec["options"], f"{path}.{key}")
class _Validator:
def __init__(self, templar):
self.templar = templar
self.errors = []
def resolve(self, value):
# Task vars arrive untemplated. Render one node at a time so a vaulted leaf is never decrypted;
# a node that cannot render here (no vault secret, a runtime-only variable) is left unchecked.
if isinstance(value, str) and self.templar.is_template(value):
try:
return self.templar.template(value)
except AnsibleError:
return UNRESOLVED
return value
def check(self, spec, value, path):
value = self.resolve(value)
if value is UNRESOLVED:
return
kind = spec.get("type", "str")
if kind == "raw" and "options" in spec and not isinstance(value, (bool, Mapping)):
self.errors.append(f"{path}: expected bool or dict, got {_kind(value)}")
return
if not TYPES[kind](value):
keyed = len(spec.get("options", {})) == 1 and next(iter(spec["options"])).startswith("<")
self.errors.append(f"{path}: expected {'dict keyed by name' if keyed else kind}, got {_kind(value)}")
return
if "choices" in spec and value not in spec["choices"]:
shown = "" if spec.get("no_log") else f", got {value!r}"
self.errors.append(f"{path}: must be one of {', '.join(map(str, spec['choices']))}{shown}")
# Keyed on the value, not the declared type, so a `raw` option ("false, or a dict of...") is
# still checked whenever it holds a dict or a list.
if "options" in spec and isinstance(value, Mapping):
self.check_options(spec["options"], value, path)
elif "elements" in spec and isinstance(value, list):
element = {"type": spec["elements"]}
if "options" in spec:
element["options"] = spec["options"]
for index, item in enumerate(value):
self.check(element, item, f"{path}[{index}]")
def check_options(self, options, value, path):
if len(options) == 1 and next(iter(options)).startswith("<"):
entry_spec = next(iter(options.values()))
for name, entry in value.items():
if entry is not None:
self.check(entry_spec, entry, f"{path}.{name}")
return
for key in value:
if key not in options:
accepted = ", ".join(sorted(options)) or "no keys"
self.errors.append(f"{path}.{key}: unsupported key, {path} accepts {accepted}")
for key, spec in options.items():
if key in value:
self.check(spec, value[key], f"{path}.{key}")
elif spec.get("required"):
self.errors.append(f"{path}.{key}: required key is missing")
class ActionModule(ActionBase):
_requires_connection = False
def run(self, tmp=None, task_vars=None):
result = super().run(tmp, task_vars)
task_vars = task_vars or {}
validator = _Validator(self._templar)
declared = {}
paths = []
# A pattern (`.../*/roles/*/meta/argument_specs.yml`) picks up every role's spec without a list
# to keep in sync; one that matches nothing is a broken path, not an empty set.
specs = self._task.args["specs"]
if not isinstance(specs, list):
raise AnsibleError(f"specs must be a list of spec file paths, got {type(specs).__name__}")
for entry in specs:
matches = sorted(glob.glob(entry)) if glob.has_magic(entry) else [entry]
if not matches:
raise AnsibleError(f"no spec file matches {entry}")
paths.extend(matches)
for path in paths:
options = self._loader.load_from_file(path)["argument_specs"]["inputs"]["options"]
_check_spec(options, path)
for name, spec in options.items():
if name in declared:
raise AnsibleError(f"{name} is declared by both {declared[name]} and {path}")
declared[name] = path
if name in task_vars:
validator.check(spec, task_vars[name], name)
elif spec.get("required"):
validator.errors.append(f"{name}: required variable is not defined")
if validator.errors:
result.update(failed=True, errors=validator.errors,
msg="Invalid input variables:\n" + "\n".join(validator.errors))
else:
result.update(changed=False, msg="All input variables are valid")
return result
-2
View File
@@ -206,7 +206,6 @@ bootstrap_debian:
- openssh-server - openssh-server
- python3 - python3
- xfsprogs - xfsprogs
- xz-utils
extra: extra:
- bat - bat
- curl - curl
@@ -259,7 +258,6 @@ bootstrap_ubuntu:
- openssh-server - openssh-server
- python3 - python3
- xfsprogs - xfsprogs
- xz-utils
extra: extra:
- bash-completion - bash-completion
- bat - bat
-3
View File
@@ -19,9 +19,6 @@ interface-name=en*;eth*;
{% set search_list = configuration_dns_search %} {% set search_list = configuration_dns_search %}
{% if iface.ip | default('') | string | length %} {% if iface.ip | default('') | string | length %}
address1={{ iface.ip }}/{{ iface.prefix }}{{ (',' ~ iface.gateway) if (iface.gateway | default('') | string | length) else '' }} address1={{ iface.ip }}/{{ iface.prefix }}{{ (',' ~ iface.gateway) if (iface.gateway | default('') | string | length) else '' }}
{% for address in iface.extra_addresses | default([]) %}
address{{ loop.index + 1 }}={{ address }}
{% endfor %}
method=manual method=manual
{% else %} {% else %}
method=auto method=auto
@@ -16,11 +16,6 @@ iface {{ ifname }} inet static
{% if loop.index0 == 0 and configuration_dns_search %} {% if loop.index0 == 0 and configuration_dns_search %}
dns-search {{ configuration_dns_search | join(' ') }} dns-search {{ configuration_dns_search | join(' ') }}
{% endif %} {% endif %}
{# One stanza per extra address: classic ifupdown takes one address per stanza, ifupdown2 merges them. #}
{% for address in iface.extra_addresses | default([]) %}
iface {{ ifname }} inet static
address {{ address }}
{% endfor %}
{% else %} {% else %}
iface {{ ifname }} inet dhcp iface {{ ifname }} inet dhcp
{% endif %} {% endif %}
@@ -9,9 +9,6 @@ network:
{% if iface.ip | default('') | string | length %} {% if iface.ip | default('') | string | length %}
addresses: addresses:
- {{ iface.ip }}/{{ iface.prefix }} - {{ iface.ip }}/{{ iface.prefix }}
{% for address in iface.extra_addresses | default([]) %}
- {{ address }}
{% endfor %}
{% if iface.gateway | default('') | string | length %} {% if iface.gateway | default('') | string | length %}
routes: routes:
- to: default - to: default
@@ -11,9 +11,6 @@ Type=ether
[Network] [Network]
{% if iface.ip | default('') | string | length %} {% if iface.ip | default('') | string | length %}
Address={{ iface.ip }}/{{ iface.prefix }} Address={{ iface.ip }}/{{ iface.prefix }}
{% for address in iface.extra_addresses | default([]) %}
Address={{ address }}
{% endfor %}
{% if iface.gateway | default('') | string | length %} {% if iface.gateway | default('') | string | length %}
Gateway={{ iface.gateway }} Gateway={{ iface.gateway }}
{% endif %} {% endif %}
@@ -1,261 +0,0 @@
---
# Validated by the validate_inputs action, not by Ansible's role argument check: no tasks file is named
# `inputs`. A sub-option named `<name>` marks a dict keyed by name.
argument_specs:
inputs:
short_description: User input read by the bootstrap and, through the main repository, by every role
options:
boot_iso: {type: str}
rhel_iso: {type: str}
custom_iso: {type: bool}
thirdparty_tasks: {type: str}
partitioning_efi_size_mib: {type: int}
partitioning_boot_size_mib: {type: int}
partitioning_separate_boot: {type: bool}
partitioning_boot_fs_fstype: {type: str}
partitioning_use_full_disk: {type: bool}
cleanup_verify_boot: {type: bool}
cleanup_boot_timeout: {type: int}
cleanup_remove_on_failure: {type: bool}
note: {type: str}
hypervisor:
type: dict
options:
type: {type: str, choices: [libvirt, proxmox, vmware, xen, none]}
url: {type: str}
username: {type: str}
password: {type: str, no_log: true}
token_id: {type: str}
token_secret: {type: str, no_log: true}
node: {type: str}
cluster: {type: str}
datacenter: {type: str}
storage: {type: str}
folder: {type: str}
certs: {type: bool}
ssh: {type: bool}
system:
type: dict
options:
type: {type: str, choices: [virtual, physical, vm]}
os: {type: str}
version: {type: str}
filesystem: {type: str, choices: [btrfs, ext4, xfs]}
name: {type: str}
source: {type: str}
id: {type: int}
cpus: {type: int}
memory: {type: int}
balloon: {type: int}
cores: {type: int}
config: {type: str, choices: [none, current, pending]}
network_info: {type: bool}
network_config: {type: str}
guest_path: {type: str}
path: {type: str}
timezone: {type: str}
locale: {type: str}
keymap: {type: str}
packages: {type: list, elements: str}
network:
type: dict
options:
bridge: {type: str}
vlan: {type: int}
ip: {type: str}
prefix: {type: int}
gateway: {type: str}
dns:
type: dict
options:
servers: {type: list, elements: str}
search: {type: list, elements: str}
interfaces:
type: list
elements: dict
options:
name: {type: str}
bridge: {type: str}
vlan: {type: int}
ip: {type: str}
prefix: {type: int}
gateway: {type: str}
extra_addresses: {type: list, elements: str}
parent: {type: str}
method: {type: str}
bridge_ports: {type: raw}
bridge_vids: {type: raw}
bridge_stp: {type: bool}
bridge_fd: {type: int}
bridge_vlan_aware: {type: bool}
bridge_pvid: {type: str}
disks:
type: list
elements: dict
options:
size: {type: int}
device: {type: str}
storage: {type: str}
format: {type: str}
bus: {type: str, choices: ["", scsi, virtio, sata, ide]}
slot: {type: int}
path: {type: str}
target: {type: str}
mount:
type: dict
options:
path: {type: str}
fstype: {type: str, choices: ["", btrfs, ext4, xfs]}
label: {type: str}
opts: {type: str}
users:
type: dict
options:
"<name>":
type: dict
options:
password: {type: str, no_log: true}
keys: {type: list, elements: str}
sudo: {type: raw}
groups: {type: list, elements: str}
shell: {type: str}
password_login: {type: bool}
uid: {type: int}
disabled: {type: bool}
root:
type: dict
options:
password: {type: str, no_log: true}
shell: {type: str}
content:
type: dict
options:
source: {type: str, choices: ["", dvd, mirror, satellite, none]}
url: {type: str}
proxy: {type: str}
gpgcheck: {type: bool}
satellite:
type: dict
options:
host: {type: str}
ip: {type: str}
org: {type: str}
activation_key: {type: str, no_log: true}
ca_url: {type: str}
service_level: {type: str}
environment: {type: str}
install: {type: bool}
luks:
type: dict
options:
enabled: {type: bool}
passphrase: {type: str, no_log: true}
mapper: {type: str}
auto: {type: bool}
method: {type: str, choices: [tpm2, keyfile]}
tpm2:
type: dict
options:
device: {type: str}
pcrs: {type: raw}
keysize: {type: int}
options: {type: str}
type: {type: str}
cipher: {type: str}
hash: {type: str}
iter: {type: int}
bits: {type: int}
pbkdf: {type: str}
features:
type: dict
options:
cloud_init: {type: bool}
cis:
type: dict
options:
enabled: {type: bool}
profile: {type: str, choices: [default, l1, l2]}
rules: {type: dict}
params: {type: dict}
selinux:
type: dict
options:
enabled: {type: bool}
firewall:
type: dict
options:
enabled: {type: bool}
backend: {type: str, choices: ["", firewalld, ufw]}
toolkit: {type: str, choices: [nftables, iptables]}
ssh:
type: dict
options:
enabled: {type: bool}
zstd:
type: dict
options:
enabled: {type: bool}
swap:
type: dict
options:
enabled: {type: bool}
banner:
type: dict
options:
motd: {type: bool}
sudo: {type: bool}
chroot:
type: dict
options:
tool: {type: str, choices: [arch-chroot, chroot, systemd-nspawn]}
initramfs:
type: dict
options:
generator: {type: str, choices: ["", dracut, mkinitcpio, initramfs-tools]}
desktop:
type: dict
options:
enabled: {type: bool}
environment: {type: str, choices: ["", gnome, kde, sway, hyprland]}
display_manager: {type: str, choices: ["", gdm, sddm, plasma-login-manager, greetd, ly]}
autologin: {type: raw}
session: {type: str}
groups: {type: list, elements: str}
secure_boot:
type: dict
options:
enabled: {type: bool}
method: {type: str, choices: ["", sbctl, uki]}
firmware:
type: dict
options:
enabled: {type: raw, choices: [true, false, auto]}
microcode: {type: raw, choices: [true, false, auto]}
gpu:
type: dict
options:
enabled: {type: raw, choices: [true, false, auto]}
nvidia_driver: {type: str, choices: [auto, open, proprietary, nouveau]}
peripherals:
type: dict
options:
enabled: {type: raw, choices: [true, false, auto]}
fingerprint: {type: raw, choices: [true, false, auto]}
camera: {type: raw, choices: [true, false, auto]}
audio: {type: raw, choices: [true, false, auto]}
bluetooth: {type: raw, choices: [true, false, auto]}
displaylink: {type: bool}
hardware:
type: dict
options:
profile: {type: dict}
cpu: {type: str}
gpus: {type: list, elements: str}
wireless: {type: list, elements: str}
audio: {type: list, elements: str}
camera: {type: dict}
fingerprint: {type: bool}
bluetooth: {type: bool}
packages: {type: dict}
disable: {type: list, elements: str}
kernel_params: {type: list, elements: str}
@@ -1,7 +1,7 @@
--- ---
- name: Build normalized system configuration - name: Build normalized system configuration
vars: vars:
system_raw: "{{ system_defaults | combine(system | default({}), recursive=True) }}" system_raw: "{{ system_defaults | combine(system, recursive=True) }}"
system_type: "{{ 'virtual' if (system_raw.type | string | lower) in ['vm', 'virtual'] else (system_raw.type | string | lower) }}" system_type: "{{ 'virtual' if (system_raw.type | string | lower) in ['vm', 'virtual'] else (system_raw.type | string | lower) }}"
system_os_input: "{{ system_raw.os | default('') | string | lower }}" system_os_input: "{{ system_raw.os | default('') | string | lower }}"
system_name: >- system_name: >-
@@ -0,0 +1,57 @@
---
- name: Ensure system input is a dictionary
ansible.builtin.set_fact:
system: "{{ system | default({}) }}"
- name: Validate system input types
ansible.builtin.assert:
that:
- system is mapping
- system.network is not defined or system.network is mapping
- system.users is not defined or system.users 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
fail_msg: "system and its nested keys (network, root, luks, features, users) must be dictionaries."
quiet: true
- name: Validate DNS lists (not strings)
when: system.network is defined and system.network.dns is defined
ansible.builtin.assert:
that:
- system.network.dns.servers is not defined or (system.network.dns.servers is iterable and system.network.dns.servers is not string)
- system.network.dns.search is not defined or (system.network.dns.search is iterable and system.network.dns.search is not string)
fail_msg: "system.network.dns.servers and system.network.dns.search must be lists, not strings."
quiet: true
- name: Validate system.users entries
when: system.users is defined and system.users is mapping and system.users | length > 0
ansible.builtin.assert:
that:
- item.value is mapping
- item.key | string | length > 0
- ('keys' not in item.value) or (item.value['keys'] is iterable and item.value['keys'] is not string)
fail_msg: "Each system.users entry must be a dict keyed by username; 'keys' must be a list."
quiet: true
loop: "{{ system.users | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: Validate system features input types
when: system.features is defined
loop: "{{ system_defaults.features | dict2items | map(attribute='key') | list }}"
loop_control:
label: "system.features.{{ item }}"
ansible.builtin.assert:
that:
- (system_defaults.features[item] is not mapping) or ((system.features[item] | default({})) is mapping)
fail_msg: "system.features.{{ item }} must be a dictionary."
quiet: true
- name: Validate system LUKS TPM2 input type
when: system.luks is defined and system.luks is mapping
ansible.builtin.assert:
that:
- system.luks.tpm2 is not defined or system.luks.tpm2 is mapping
fail_msg: "system.luks.tpm2 must be a dictionary."
quiet: true
+5 -19
View File
@@ -1,11 +1,6 @@
--- ---
# Normalizes all input dicts into system_cfg/hypervisor_cfg/etc. here, so downstream # Normalizes all input dicts into system_cfg/hypervisor_cfg/etc. here, so downstream
# roles consume the computed facts directly with no per-role _normalize (except CIS). # roles consume the computed facts directly with no per-role _normalize (except CIS).
- name: Validate user input
validate_inputs:
specs:
- "{{ role_path }}/meta/argument_specs.yml"
- name: Global defaults loaded - name: Global defaults loaded
ansible.builtin.debug: ansible.builtin.debug:
msg: Global defaults loaded. msg: Global defaults loaded.
@@ -52,17 +47,6 @@
ansible.builtin.set_fact: ansible.builtin.set_fact:
system_cfg: "{{ system_cfg | combine({'path': hypervisor_cfg.folder | string}, recursive=True) }}" system_cfg: "{{ system_cfg | combine({'path': hypervisor_cfg.folder | string}, recursive=True) }}"
# Guarded so a malformed users input reaches the validation message, not a filter error.
- name: Pick the primary user (first with a password)
vars:
_first: >-
{{ (system_cfg.users if system_cfg.users is mapping else {}) | dict2items
| selectattr('value', 'mapping') | selectattr('value.password', 'defined')
| rejectattr('value.password', 'equalto', '') | first | default({}) }}
ansible.builtin.set_fact:
bootstrap_primary_user: "{{ {'name': _first.key, 'password': _first.value.password} if _first else {} }}"
no_log: true
- name: Validate variables - name: Validate variables
ansible.builtin.include_tasks: validation.yml ansible.builtin.include_tasks: validation.yml
@@ -105,12 +89,14 @@
when: when:
- system_cfg.type == "virtual" - system_cfg.type == "virtual"
- hypervisor_type != "vmware" - hypervisor_type != "vmware"
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
ansible_host: "{{ system_cfg.network.ip }}" ansible_host: "{{ system_cfg.network.ip }}"
ansible_port: 22 ansible_port: 22
ansible_user: "{{ bootstrap_primary_user.name }}" ansible_user: "{{ _primary.key }}"
ansible_password: "{{ bootstrap_primary_user.password }}" ansible_password: "{{ _primary.value.password }}"
ansible_become_password: "{{ bootstrap_primary_user.password }}" ansible_become_password: "{{ _primary.value.password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
no_log: true no_log: true
+4 -3
View File
@@ -5,6 +5,9 @@
- 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:
- name: Validate raw system input types
ansible.builtin.include_tasks: _validate_input.yml
- name: Normalize system configuration - name: Normalize system configuration
ansible.builtin.include_tasks: _normalize_system.yml ansible.builtin.include_tasks: _normalize_system.yml
@@ -32,12 +35,10 @@
}, recursive=True) }, recursive=True)
}} }}
# Keyed on os, not hostname: inventories set hostname themselves, and doing so
# would otherwise skip enrichment and leave os undefined for validation.
- name: Check if pre-computed system_cfg needs enrichment - name: Check if pre-computed system_cfg needs enrichment
when: system_cfg is defined when: system_cfg is defined
ansible.builtin.set_fact: ansible.builtin.set_fact:
_bootstrap_needs_enrichment: "{{ os is not defined }}" _bootstrap_needs_enrichment: "{{ hostname is not defined }}"
- name: Merge pre-computed system_cfg with bootstrap system_defaults - name: Merge pre-computed system_cfg with bootstrap system_defaults
when: when:
+104 -16
View File
@@ -26,6 +26,90 @@
fail_msg: "hypervisor.type must be one of: libvirt, proxmox, vmware, xen when system.type=virtual." fail_msg: "hypervisor.type must be one of: libvirt, proxmox, vmware, xen when system.type=virtual."
quiet: true quiet: true
- name: Validate hypervisor schema
vars:
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:
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: "{{ 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(', ') }}."
quiet: true
- name: Validate system sub-dict schemas
loop:
- network
- root
- luks
loop_control:
label: "system.{{ item }}"
vars:
sub_input: "{{ (system[item] | default({})) | dict2items | map(attribute='key') | list }}"
sub_allowed: "{{ system_defaults[item] | dict2items | map(attribute='key') | list }}"
sub_unknown: "{{ sub_input | difference(sub_allowed) }}"
ansible.builtin.assert:
that:
- sub_unknown | length == 0
fail_msg: "Unsupported system.{{ item }} keys: {{ sub_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 | selectattr('value', 'mapping') }}"
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 - name: Validate OS and version inputs
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -270,12 +354,28 @@
quiet: true quiet: true
- name: Validate at least one user with a password is defined - name: Validate at least one user with a password is defined
vars:
_pw_users: "{{ system_cfg.users | dict2items | selectattr('value.password', 'defined') | list }}"
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- bootstrap_primary_user | length > 0 - system_cfg.users | default({}) | length > 0
fail_msg: >- - _pw_users | length > 0
At least one user in system.users needs a non-empty password: bootstrap logs in - _pw_users[0].key | string | length > 0
as the first such user after the reboot. - _pw_users[0].value.password | string | length > 0
fail_msg: "At least one user with a password must be defined in system.users."
quiet: true
no_log: true
- name: Validate DNS servers is a list
when:
- system_cfg.network.dns.servers is defined
- system_cfg.network.dns.servers | length > 0
ansible.builtin.assert:
that:
- system_cfg.network.dns.servers is iterable
- system_cfg.network.dns.servers is not string
- system_cfg.network.dns.servers is not mapping
fail_msg: "system.network.dns.servers must be a list."
quiet: true quiet: true
- name: Validate all virtual disks have a positive size - name: Validate all virtual disks have a positive size
@@ -369,18 +469,6 @@
loop_control: loop_control:
label: "{{ item | to_json }}" label: "{{ item | to_json }}"
- name: Validate network interfaces extra_addresses
when: item.extra_addresses is defined
ansible.builtin.assert:
that:
- item.ip | default('') | string | length > 0
- item.extra_addresses | reject('match', '^(\\d{1,3}\\.){3}\\d{1,3}/\\d{1,2}$') | list | length == 0
fail_msg: "system.network.interfaces[].extra_addresses must be a list of IPv4 CIDRs (192.0.2.10/24) on an entry that also sets 'ip'."
quiet: true
loop: "{{ system_cfg.network.interfaces | default([]) }}"
loop_control:
label: "{{ item | to_json }}"
- name: Validate hostname format - name: Validate hostname format
ansible.builtin.assert: ansible.builtin.assert:
that: that:
+2 -2
View File
@@ -35,8 +35,8 @@
{%- endfor -%} {%- endfor -%}
{{ out }} {{ out }}
community.proxmox.proxmox_kvm: community.proxmox.proxmox_kvm:
ciuser: "{{ bootstrap_primary_user.name }}" ciuser: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).key }}"
cipassword: "{{ bootstrap_primary_user.password }}" cipassword: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).value.password }}"
ciupgrade: false ciupgrade: false
vmid: "{{ system_cfg.id }}" vmid: "{{ system_cfg.id }}"
name: "{{ hostname }}" name: "{{ hostname }}"
@@ -12,9 +12,6 @@ network:
{% if has_static %} {% if has_static %}
addresses: addresses:
- "{{ iface.ip }}/{{ iface.prefix }}" - "{{ iface.ip }}/{{ iface.prefix }}"
{% for address in iface.extra_addresses | default([]) %}
- "{{ address }}"
{% endfor %}
{% if iface.gateway | default('') | string | length %} {% if iface.gateway | default('') | string | length %}
routes: routes:
- to: default - to: default
@@ -14,9 +14,8 @@ users:
{% elif _sudo | bool %} {% elif _sudo | bool %}
sudo: "ALL=(ALL) NOPASSWD:ALL" sudo: "ALL=(ALL) NOPASSWD:ALL"
{% endif %} {% endif %}
{% set _pw = attrs.password | default('') %} {% if attrs.password | default('') | length > 0 %}
{% if _pw | length > 0 %} passwd: "{{ attrs.password | password_hash('sha512') }}"
passwd: "{{ _pw if (_pw | string)[:1] == '$' else _pw | password_hash('sha512') }}"
lock_passwd: false lock_passwd: false
{% else %} {% else %}
lock_passwd: true lock_passwd: true
+1 -1
View File
@@ -34,4 +34,4 @@ merge_fixtures:
disable: [nvidia, intel] # drop the nvidia GPU and every intel-vendor contribution disable: [nvidia, intel] # drop the nvidia GPU and every intel-vendor contribution
expect_profile: { fingerprint: false, bluetooth: false } expect_profile: { fingerprint: false, bluetooth: false }
expect_contains: [mesa, vulkan-icd-loader] expect_contains: [mesa, vulkan-icd-loader]
expect_excludes: [nvidia-open-dkms, intel-ucode, vulkan-intel, sof-firmware, linux-firmware-intel] expect_excludes: [nvidia-open-dkms, intel-ucode, vulkan-intel, sof-firmware, linux-firmware-other]
+1 -1
View File
@@ -5,7 +5,7 @@ package_fixtures:
- name: arch-intel-audio-bt-fp - name: arch-intel-audio-bt-fp
os_family: Archlinux os_family: Archlinux
profile: { cpu: intel, gpus: [intel], wireless: [intel], audio: [intel], fingerprint: true, bluetooth: true, camera: { uvc: true, ipu6: false } } profile: { cpu: intel, gpus: [intel], wireless: [intel], audio: [intel], fingerprint: true, bluetooth: true, camera: { uvc: true, ipu6: false } }
expect_contains: [intel-ucode, linux-firmware-intel, mesa, vulkan-icd-loader, vulkan-intel, sof-firmware, alsa-ucm-conf, bluez, bluez-utils, fprintd, libfprint, v4l-utils] expect_contains: [intel-ucode, linux-firmware-other, mesa, vulkan-icd-loader, vulkan-intel, sof-firmware, alsa-ucm-conf, bluez, bluez-utils, fprintd, libfprint, v4l-utils]
expect_excludes: [nvidia-open-dkms, evdi] expect_excludes: [nvidia-open-dkms, evdi]
- name: debian-amd-audio-no-bt-no-fp - name: debian-amd-audio-no-bt-no-fp
-22
View File
@@ -1,22 +0,0 @@
---
- name: "Validate the fixture ({{ inf.name }})"
block:
- name: "Run the validator ({{ inf.name }})"
validate_inputs:
specs:
- "{{ playbook_dir }}/fixture_spec.yml"
vars:
fixture: "{{ inf.value }}"
register: inputs_result
rescue:
- name: "Keep the failed result ({{ inf.name }})"
ansible.builtin.set_fact:
inputs_result: "{{ ansible_failed_result }}"
- name: "Assert the reported violations ({{ inf.name }})"
ansible.builtin.assert:
that:
- (inputs_result.errors | default([])) | sort == inf.expect | sort
- "'topsecret-value' not in (inputs_result | to_json)"
fail_msg: "[inputs {{ inf.name }}] expected {{ inf.expect }}, got {{ inputs_result.errors | default([]) }}"
success_msg: "[inputs {{ inf.name }}] OK"
-47
View File
@@ -1,47 +0,0 @@
---
argument_specs:
inputs:
options:
fixture:
type: dict
options:
name:
type: str
required: true
enabled:
type: bool
count:
type: int
servers:
type: list
elements: str
kind:
type: str
choices: [a, b]
secret:
type: str
no_log: true
gate:
type: raw
options:
port:
type: int
disks:
type: list
elements: dict
options:
size:
type: int
required: true
users:
type: dict
options:
"<user>":
type: dict
options:
password:
type: str
no_log: true
keys:
type: list
elements: str
-18
View File
@@ -1,18 +0,0 @@
---
# Vaulted with a discarded password: the validator must handle a value it cannot decrypt.
fixture:
name: web01
secret: !vault |
$ANSIBLE_VAULT;1.1;AES256
62363835313830363035333361613236373662303039376639396136336438616331336565393631
6637376163346430386663393661386561316537306662350a663563326164376164396230616361
33646564346532643735396239626133333162353839666135343030643836313938363466383439
3130353636666365310a353064373139323836373163336563353861356333316534386238333664
3663
servers: !vault |
$ANSIBLE_VAULT;1.1;AES256
65626131373433633432363261356534613135386461626535393539656631323531346661643233
3961366535643630386462303038636231373239343130310a343565336461633532316136643139
30306461616232663763633630353661376130323536346637633964373239343664303533396236
6165623262633131330a303965643439626335663130376237393362363231346537643334633163
3835
-122
View File
@@ -1,122 +0,0 @@
---
inputs_fixtures:
- name: valid
value:
name: web01
enabled: true
count: "3"
servers: [10.0.0.1, 10.0.0.2]
kind: a
disks:
- size: 30
users:
alice:
password: pw
keys: [ssh-ed25519 AAAA alice]
retired: ~
expect: []
- name: empty-int-is-unset
value:
name: web01
count: ""
expect: []
- name: list-for-dict
value:
name: web01
users:
- name: alice
expect: ["fixture.users: expected dict keyed by name, got list"]
- name: comma-string-for-list
value:
name: web01
servers: 10.0.0.1,10.0.0.2
expect: ["fixture.servers: expected list, got str"]
- name: yes-string-for-bool
value:
name: web01
enabled: "yes"
expect: ["fixture.enabled: expected bool, got str"]
- name: typo-key
value:
name: web01
userz: {}
expect: ["fixture.userz: unsupported key, fixture accepts count, disks, enabled, gate, kind, name, secret, servers, users"]
- name: wrong-element-type
value:
name: web01
disks:
- size: 30
- size: big
expect: ["fixture.disks[1].size: expected int, got str"]
- name: missing-required
value:
enabled: false
expect: ["fixture.name: required key is missing"]
- name: choice-mismatch
value:
name: web01
kind: c
expect: ["fixture.kind: must be one of a, b, got 'c'"]
- name: secrets-never-echoed
value:
name: web01
secret: [topsecret-value]
users:
alice:
password: [topsecret-value]
expect:
- "fixture.secret: expected str, got list"
- "fixture.users.alice.password: expected str, got list"
- name: keyed-entry-not-a-dict
value:
name: web01
users:
alice: pw
expect: ["fixture.users.alice: expected dict, got str"]
- name: two-violations-at-once
value:
name: web01
enabled: "true"
userz: {}
expect:
- "fixture.userz: unsupported key, fixture accepts count, disks, enabled, gate, kind, name, secret, servers, users"
- "fixture.enabled: expected bool, got str"
- name: raw-gate-false
value:
name: web01
gate: false
expect: []
- name: raw-gate-dict-checked
value:
name: web01
gate:
port: http
portz: 1
expect:
- "fixture.gate.portz: unsupported key, fixture.gate accepts port"
- "fixture.gate.port: expected int, got str"
- name: raw-gate-true
value:
name: web01
gate: true
expect: []
- name: raw-gate-string-rejected
value:
name: web01
gate: "yes"
expect: ["fixture.gate: expected bool or dict, got str"]
-108
View File
@@ -1,108 +0,0 @@
---
# Run: ansible-playbook tests/inputs/test_inputs.yml
- name: Strict input validator fixture tests
hosts: localhost
gather_facts: false
connection: local
vars_files:
- inputs_fixtures.yml
tasks:
- name: Run each input fixture
ansible.builtin.include_tasks: _assert_inputs.yml
loop: "{{ inputs_fixtures }}"
loop_control:
loop_var: inf
label: "{{ inf.name }}"
- name: Expand a spec pattern
validate_inputs:
specs:
- "{{ playbook_dir }}/fixture_*.yml"
vars:
fixture:
name: web01
servers: [10.0.0.1]
register: inputs_pattern_result
- name: Assert the pattern matched the fixture spec
ansible.builtin.assert:
that:
- inputs_pattern_result is succeeded
success_msg: "[inputs pattern] OK"
- name: Refuse a pattern that matches nothing
block:
- name: Run the validator with a pattern that matches no file
validate_inputs:
specs:
- "{{ playbook_dir }}/nothing_*.yml"
register: inputs_nomatch_result
rescue:
- name: Keep the no-match failure
ansible.builtin.set_fact:
inputs_nomatch_result: "{{ ansible_failed_result }}"
- name: Assert the empty pattern was refused
ansible.builtin.assert:
that:
- inputs_nomatch_result.msg is search('no spec file matches')
fail_msg: "[inputs no-match] got {{ inputs_nomatch_result.msg | default('') }}"
success_msg: "[inputs no-match] OK"
- name: Refuse a spec list given as a string
block:
- name: Run the validator with specs as a string
validate_inputs:
specs: "{{ playbook_dir }}/fixture_spec.yml"
register: inputs_string_result
rescue:
- name: Keep the string-specs failure
ansible.builtin.set_fact:
inputs_string_result: "{{ ansible_failed_result }}"
- name: Assert the string was refused
ansible.builtin.assert:
that:
- inputs_string_result.msg is search('specs must be a list')
fail_msg: "[inputs string] got {{ inputs_string_result.msg | default('') }}"
success_msg: "[inputs string] OK"
- name: Refuse a variable declared by two specs
block:
- name: Run the validator with the fixture spec twice
validate_inputs:
specs:
- "{{ playbook_dir }}/fixture_spec.yml"
- "{{ playbook_dir }}/fixture_spec.yml"
register: inputs_duplicate_result
rescue:
- name: Keep the duplicate-declaration failure
ansible.builtin.set_fact:
inputs_duplicate_result: "{{ ansible_failed_result }}"
- name: Assert the duplicate declaration was refused
ansible.builtin.assert:
that:
- inputs_duplicate_result.msg is search('fixture is declared by both')
fail_msg: "[inputs duplicate] got {{ inputs_duplicate_result.msg | default('') }}"
success_msg: "[inputs duplicate] OK"
- name: Validate a host var holding values that cannot be decrypted
block:
- name: Run the validator on the vaulted host var
validate_inputs:
specs:
- "{{ playbook_dir }}/fixture_spec.yml"
register: inputs_vault_result
rescue:
- name: Keep the failed vault result
ansible.builtin.set_fact:
inputs_vault_result: "{{ ansible_failed_result }}"
- name: Assert a vaulted string passes and a vaulted non-list is reported
ansible.builtin.assert:
that:
- >-
inputs_vault_result.errors | default([]) == ['fixture.servers: expected list, got str']
fail_msg: "[inputs vault] got {{ inputs_vault_result.errors | default([]) }}"
success_msg: "[inputs vault] OK"
-62
View File
@@ -1,62 +0,0 @@
---
# Run: ansible-playbook tests/users/test_users.yml
# One host per fixture: set_fact state cannot be cleared between loop iterations.
- name: Register one host per users fixture
hosts: localhost
gather_facts: false
connection: local
vars_files:
- users_fixtures.yml
tasks:
- name: Add the fixture host
ansible.builtin.add_host:
name: "{{ uf.name }}"
groups: users_fixtures
ansible_connection: local
ansible_python_interpreter: "{{ ansible_playbook_python }}"
expect: "{{ uf.expect }}"
system: "{{ _system }}"
system_cfg: "{{ _system if uf.precomputed | default(false) else omit }}"
vars:
_system: "{{ users_fixture_system | combine(uf.system, {'name': uf.name}, recursive=True) }}"
loop: "{{ users_fixtures }}"
loop_control:
loop_var: uf
label: "{{ uf.name }}"
- name: Run global_defaults against each users fixture
hosts: users_fixtures
gather_facts: false
vars:
hypervisor:
type: libvirt
url: qemu:///system
storage: default
boot_iso: /var/lib/libvirt/images/fixture.iso
tasks:
- name: Normalize and validate
block:
- name: Load global defaults
ansible.builtin.include_role:
name: global_defaults
- name: Record the pass
ansible.builtin.set_fact:
users_outcome: pass
rescue:
- name: Record the failure
ansible.builtin.set_fact:
users_outcome: >-
{{ ([ansible_failed_result.msg | default('')]
+ (ansible_failed_result.results | default([]) | map(attribute='msg', default='') | list))
| join(' ') }}
- name: Assert the expected outcome
ansible.builtin.assert:
that:
- >-
(users_outcome == 'pass' and bootstrap_primary_user.name == expect.primary)
if expect.primary is defined
else (expect.fail in users_outcome)
fail_msg: "[{{ inventory_hostname }}] expected {{ expect }}, got: {{ users_outcome }}"
success_msg: "[{{ inventory_hostname }}] OK"
-123
View File
@@ -1,123 +0,0 @@
---
users_fixture_system:
type: virtual
os: debian
version: "13"
cpus: 2
memory: 2048
disks:
- size: 30
network:
interfaces:
- bridge: br0
ip: 192.0.2.10
prefix: 24
gateway: 192.0.2.1
root:
password: rootpw
# precomputed: hand system_cfg over the way deploy_iac does, skipping the raw input checks.
users_fixtures:
- name: dict-first-password-wins
system:
users:
keyonly:
keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFixture keyonly
blank:
password: ""
ops:
password: pw
sudo: true
later:
password: pw2
expect:
primary: ops
- name: dict-first-password-wins-precomputed
precomputed: true
system:
users:
keyonly:
keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFixture keyonly
ops:
password: pw
expect:
primary: ops
- name: list-rejected
system:
users:
- name: ops
password: pw
expect:
fail: "system.users: expected dict keyed by name, got list"
- name: list-rejected-precomputed
precomputed: true
system:
users:
- name: ops
password: pw
expect:
fail: "system.users: expected dict keyed by name, got list"
- name: scalar-entry-rejected
system:
users:
ops: pw
expect:
fail: "system.users.ops: expected dict, got str"
- name: scalar-entry-rejected-precomputed
precomputed: true
system:
users:
ops: pw
expect:
fail: "system.users.ops: expected dict, got str"
- name: keys-string-rejected
system:
users:
ops:
password: pw
keys: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFixture ops
expect:
fail: "system.users.ops.keys: expected list, got str"
- name: no-password-rejected
system:
users:
ops:
keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFixture ops
expect:
fail: needs a non-empty password
- name: autologin-user-accepted
system:
users:
ops:
password: pw
features:
desktop:
enabled: true
environment: gnome
autologin: ops
expect:
primary: ops
- name: autologin-unknown-user-rejected
system:
users:
ops:
password: pw
features:
desktop:
enabled: true
environment: gnome
autologin: ghost
expect:
fail: desktop.autologin must be false or a username