Compare commits
86
Commits
master
..
43755a6016
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43755a6016 | ||
|
|
a795535305 | ||
|
|
a9a9706f2c | ||
|
|
f5bb5e92be | ||
|
|
22e66959ee | ||
|
|
332dfeb6f1 | ||
|
|
19029bdc8b | ||
|
|
9c4a86c0ec | ||
|
|
723f0516da | ||
|
|
975384661e | ||
|
|
ad25bec9fd | ||
|
|
6240217318 | ||
|
|
1c00ec9370 | ||
|
|
371d5a0acd | ||
|
|
41ccf2a5b9 | ||
|
|
8f9cfe3b2f | ||
|
|
6a75237197 | ||
|
|
b04aad12fb | ||
|
|
4fff9f8d80 | ||
|
|
7f12a0f3d8 | ||
|
|
ceb2237bbb | ||
|
|
477c8379c4 | ||
|
|
579c499c02 | ||
|
|
89e366d0f0 | ||
|
|
6fe843355e | ||
|
|
441876fab9 | ||
|
|
00acd4d200 | ||
|
|
d922efd2e4 | ||
|
|
939c5c741f | ||
|
|
2c35409519 | ||
|
|
d2a19cfd5c | ||
|
|
44f5adc682 | ||
|
|
0185797af9 | ||
|
|
e0ecf628cd | ||
|
|
37df881daa | ||
|
|
55b21eae5d | ||
|
|
b1e938b7f0 | ||
|
|
c843f5289b | ||
|
|
9757ed3785 | ||
|
|
876e90ce2b | ||
|
|
7c44cb1ff0 | ||
|
|
5d0630a386 | ||
|
|
3eaf918a53 | ||
|
|
382e82ff85 | ||
|
|
db7dc53bd7 | ||
|
|
7d45f25a7e | ||
|
|
3880b8f41e | ||
|
|
dc3c4a901f | ||
|
|
6bfaa0aa2b | ||
|
|
5dc0424acb | ||
|
|
c12332a853 | ||
|
|
5ace4577a2 | ||
|
|
c0ff01ef40 | ||
|
|
57417514e3 | ||
|
|
0928588c1f | ||
|
|
6d622f2db4 | ||
|
|
b11d65a6f3 | ||
|
|
3623fc292c | ||
|
|
dfca7ec94b | ||
|
|
e8be84bf49 | ||
|
|
322cc0b1ce | ||
|
|
4b38754f8b | ||
|
|
a6bc7ffe04 | ||
|
|
c529e71ebc | ||
|
|
cb46de2b6d | ||
|
|
9169117b25 | ||
|
|
6c94c519fb | ||
|
|
efd96a42b8 | ||
|
|
68661c3cca | ||
|
|
1db20c7ac0 | ||
|
|
7b155b427b | ||
|
|
ca8721e98f | ||
|
|
cdb2559d8f | ||
|
|
443f6623df | ||
|
|
6cf418fe00 | ||
|
|
47ec5fe621 | ||
|
|
240f945cce | ||
|
|
663a04556f | ||
|
|
6febd1acf1 | ||
|
|
008187860c | ||
|
|
cd1be6b5e1 | ||
|
|
15be6149fd | ||
|
|
ca29ad200d | ||
|
|
8079099cee | ||
|
|
9e79185b07 | ||
|
|
b88bf2860f |
No files matched your search
@@ -9,6 +9,3 @@ vars_libvirt.yml
|
|||||||
vars_proxmox.yml
|
vars_proxmox.yml
|
||||||
|
|
||||||
.sisyphus/
|
.sisyphus/
|
||||||
|
|
||||||
.ansible/
|
|
||||||
__pycache__/
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
---
|
---
|
||||||
extends: default
|
extends: default
|
||||||
|
|
||||||
ignore:
|
|
||||||
- .ansible/
|
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
document-start: disable
|
document-start: disable
|
||||||
line-length:
|
line-length:
|
||||||
|
|||||||
@@ -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` |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
Reference in new issue
Block a user