Compare commits
81
Commits
master
..
332dfeb6f1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
|
||||
.sisyphus/
|
||||
|
||||
.ansible/
|
||||
__pycache__/
|
||||
@@ -1,9 +1,6 @@
|
||||
---
|
||||
extends: default
|
||||
|
||||
ignore:
|
||||
- .ansible/
|
||||
|
||||
rules:
|
||||
document-start: disable
|
||||
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) |
|
||||
| `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`
|
||||
|
||||
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
|
||||
system:
|
||||
@@ -238,11 +238,6 @@ system:
|
||||
| `password` | string | -- | User password (required for at least one user) |
|
||||
| `keys` | list | `[]` | SSH public keys |
|
||||
| `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`.
|
||||
|
||||
@@ -543,13 +538,10 @@ When `hypervisor.type: vmware` uses the `vmware_tools` connection:
|
||||
|
||||
`system.disks[0]` is the OS disk (no `mount.path`). Additional entries define data disks.
|
||||
|
||||
On VMware, `device` is resolved on the target from the SCSI address instead: entry N is the disk at target N on the first SCSI controller (N+1 from index 7 on, since unit 7 is reserved). The kernel's `sdX` order is not stable between the install environment and the installed system, so a letter-derived path can point at another disk.
|
||||
|
||||
| Key | Type | Description |
|
||||
| ------------- | ------ | ------------------------------------------------------ |
|
||||
| `size` | number | Disk size in GB (required for virtual) |
|
||||
| `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) |
|
||||
| `mount.path` | string | Mount point (additional disks only) |
|
||||
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
[defaults]
|
||||
hash_behaviour = merge
|
||||
roles_path = roles
|
||||
action_plugins = plugins/action
|
||||
interpreter_python = auto_silent
|
||||
deprecation_warnings = False
|
||||
host_key_checking = False
|
||||
|
||||
@@ -119,13 +119,15 @@
|
||||
when:
|
||||
- post_reboot_can_connect | bool
|
||||
no_log: true
|
||||
vars:
|
||||
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
|
||||
ansible.builtin.set_fact:
|
||||
ansible_connection: ssh
|
||||
ansible_host: "{{ system_cfg.network.ip }}"
|
||||
ansible_port: 22
|
||||
ansible_user: "{{ bootstrap_primary_user.name }}"
|
||||
ansible_password: "{{ bootstrap_primary_user.password }}"
|
||||
ansible_become_password: "{{ bootstrap_primary_user.password }}"
|
||||
ansible_user: "{{ _primary.key }}"
|
||||
ansible_password: "{{ _primary.value.password }}"
|
||||
ansible_become_password: "{{ _primary.value.password }}"
|
||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
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
|
||||
- python3
|
||||
- xfsprogs
|
||||
- xz-utils
|
||||
extra:
|
||||
- bat
|
||||
- curl
|
||||
@@ -259,7 +258,6 @@ bootstrap_ubuntu:
|
||||
- openssh-server
|
||||
- python3
|
||||
- xfsprogs
|
||||
- xz-utils
|
||||
extra:
|
||||
- bash-completion
|
||||
- bat
|
||||
|
||||
@@ -12,21 +12,6 @@
|
||||
and 'No such file or directory' not in (_firewall_enable.stderr | default(''))
|
||||
and 'does not exist' not in (_firewall_enable.stderr | default(''))
|
||||
|
||||
# Distro base sets ship the daemon preset-enabled (RHEL @core pulls firewalld in),
|
||||
# so opting out has to disable the unit, not merely skip enabling it.
|
||||
- name: Disable the firewall daemon in the install chroot
|
||||
when:
|
||||
- firewall_phase == 'install'
|
||||
- _configuration_platform.init_system == 'systemd'
|
||||
- not system_cfg.features.firewall.enabled | bool
|
||||
ansible.builtin.command: "{{ chroot_command }} systemctl disable {{ system_cfg.features.firewall.backend }}"
|
||||
register: _firewall_disable
|
||||
changed_when: "'Removed' in (_firewall_disable.stderr | default(''))"
|
||||
failed_when: >-
|
||||
_firewall_disable.rc != 0
|
||||
and 'No such file or directory' not in (_firewall_disable.stderr | default(''))
|
||||
and 'does not exist' not in (_firewall_disable.stderr | default(''))
|
||||
|
||||
# ufw's CLI needs a running kernel and is a no-op in the chroot (leaves ENABLED=no),
|
||||
# so its activation and SSH rule are applied here, after reboot.
|
||||
- name: Allow SSH through ufw before enabling
|
||||
|
||||
@@ -43,14 +43,11 @@
|
||||
}}
|
||||
grub_root_flags: >-
|
||||
{{ ['rootflags=subvol=@'] if system_cfg.filesystem == 'btrfs' else [] }}
|
||||
grub_selinux_args: >-
|
||||
{{ [] if system_cfg.features.selinux.enabled | bool else ['selinux=0'] }}
|
||||
# String-concat (not list-concat like grub_kernel_cmdline_base below): ansible-lint's
|
||||
# jinja render trips on list+list when grub_lvm_args leads the expression here.
|
||||
grub_cmdline_linux_base: >-
|
||||
{{
|
||||
((grub_lvm_args | join(' ')) ~ ' ' ~ (grub_selinux_args | join(' ')) ~ ' '
|
||||
~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
|
||||
((grub_lvm_args | join(' ')) ~ ' ' ~ (_hardware_profile_kernel_params | default([]) | join(' '))) | trim
|
||||
}}
|
||||
grub_kernel_cmdline_base: >-
|
||||
{{
|
||||
@@ -60,7 +57,6 @@
|
||||
+ ['ro']
|
||||
+ grub_lvm_args
|
||||
+ grub_root_flags
|
||||
+ grub_selinux_args
|
||||
+ (_hardware_profile_kernel_params | default([]))
|
||||
)
|
||||
| join(' ')
|
||||
|
||||
@@ -22,12 +22,10 @@
|
||||
mode: "0644"
|
||||
|
||||
# Fedora: setfiles segfaults during bootstrap chroot relabeling, so SELinux
|
||||
# is left permissive and expected to relabel on first boot. Opting out writes
|
||||
# disabled here and selinux=0 on the kernel cmdline (grub.yml), the only form
|
||||
# EL9 honours.
|
||||
- name: Set the SELinux mode
|
||||
# is left permissive and expected to relabel on first boot.
|
||||
- name: Disable SELinux
|
||||
when: os == "fedora" or not system_cfg.features.selinux.enabled | bool
|
||||
ansible.builtin.lineinfile:
|
||||
path: /mnt/etc/selinux/config
|
||||
regexp: ^SELINUX=
|
||||
line: "SELINUX={{ 'permissive' if system_cfg.features.selinux.enabled | bool else 'disabled' }}"
|
||||
line: SELINUX=permissive
|
||||
@@ -19,9 +19,6 @@ interface-name=en*;eth*;
|
||||
{% set search_list = configuration_dns_search %}
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
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
|
||||
{% else %}
|
||||
method=auto
|
||||
|
||||
@@ -16,11 +16,6 @@ iface {{ ifname }} inet static
|
||||
{% if loop.index0 == 0 and configuration_dns_search %}
|
||||
dns-search {{ configuration_dns_search | join(' ') }}
|
||||
{% 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 %}
|
||||
iface {{ ifname }} inet dhcp
|
||||
{% endif %}
|
||||
|
||||
@@ -9,9 +9,6 @@ network:
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
addresses:
|
||||
- {{ iface.ip }}/{{ iface.prefix }}
|
||||
{% for address in iface.extra_addresses | default([]) %}
|
||||
- {{ address }}
|
||||
{% endfor %}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
routes:
|
||||
- to: default
|
||||
|
||||
@@ -11,9 +11,6 @@ Type=ether
|
||||
[Network]
|
||||
{% if iface.ip | default('') | string | length %}
|
||||
Address={{ iface.ip }}/{{ iface.prefix }}
|
||||
{% for address in iface.extra_addresses | default([]) %}
|
||||
Address={{ address }}
|
||||
{% endfor %}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
Gateway={{ iface.gateway }}
|
||||
{% 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}
|
||||
@@ -94,7 +94,6 @@
|
||||
|
||||
- name: Set install_drive from primary disk
|
||||
when:
|
||||
- install_drive is not defined
|
||||
- system_disks_cfg | length > 0
|
||||
- system_disks_cfg[0].device | string | length > 0
|
||||
ansible.builtin.set_fact:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
- name: Build normalized system configuration
|
||||
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_os_input: "{{ system_raw.os | default('') | string | lower }}"
|
||||
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
|
||||
# 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
|
||||
ansible.builtin.debug:
|
||||
msg: Global defaults loaded.
|
||||
@@ -52,17 +47,6 @@
|
||||
ansible.builtin.set_fact:
|
||||
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
|
||||
ansible.builtin.include_tasks: validation.yml
|
||||
|
||||
@@ -105,12 +89,14 @@
|
||||
when:
|
||||
- system_cfg.type == "virtual"
|
||||
- hypervisor_type != "vmware"
|
||||
vars:
|
||||
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
|
||||
ansible.builtin.set_fact:
|
||||
ansible_host: "{{ system_cfg.network.ip }}"
|
||||
ansible_port: 22
|
||||
ansible_user: "{{ bootstrap_primary_user.name }}"
|
||||
ansible_password: "{{ bootstrap_primary_user.password }}"
|
||||
ansible_become_password: "{{ bootstrap_primary_user.password }}"
|
||||
ansible_user: "{{ _primary.key }}"
|
||||
ansible_password: "{{ _primary.value.password }}"
|
||||
ansible_become_password: "{{ _primary.value.password }}"
|
||||
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
|
||||
no_log: true
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
- name: Normalize system and disk configuration
|
||||
when: system_cfg is not defined
|
||||
block:
|
||||
- name: Validate raw system input types
|
||||
ansible.builtin.include_tasks: _validate_input.yml
|
||||
|
||||
- name: Normalize system configuration
|
||||
ansible.builtin.include_tasks: _normalize_system.yml
|
||||
|
||||
@@ -32,12 +35,10 @@
|
||||
}, 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
|
||||
when: system_cfg is defined
|
||||
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
|
||||
when:
|
||||
@@ -78,7 +79,9 @@
|
||||
os_version: "{{ system_cfg.version | default('') | string }}"
|
||||
|
||||
- name: Normalize disk configuration (pre-computed system_cfg)
|
||||
when: system_cfg is defined
|
||||
when:
|
||||
- system_cfg is defined
|
||||
- install_drive is not defined
|
||||
ansible.builtin.include_tasks: _normalize_disks.yml
|
||||
|
||||
# Runs on every path before validation, so an empty firewall.backend / content.source
|
||||
|
||||
@@ -26,6 +26,90 @@
|
||||
fail_msg: "hypervisor.type must be one of: libvirt, proxmox, vmware, xen when system.type=virtual."
|
||||
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
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
@@ -270,12 +354,28 @@
|
||||
quiet: true
|
||||
|
||||
- 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:
|
||||
that:
|
||||
- bootstrap_primary_user | length > 0
|
||||
fail_msg: >-
|
||||
At least one user in system.users needs a non-empty password: bootstrap logs in
|
||||
as the first such user after the reboot.
|
||||
- system_cfg.users | default({}) | length > 0
|
||||
- _pw_users | length > 0
|
||||
- _pw_users[0].key | string | length > 0
|
||||
- _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
|
||||
|
||||
- name: Validate all virtual disks have a positive size
|
||||
@@ -369,18 +469,6 @@
|
||||
loop_control:
|
||||
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
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
# The virtualization role attaches system.disks to SCSI controller 0 in list order, and
|
||||
# vSphere never gives a disk unit 7 (the controller's own address). The kernel's sdX order
|
||||
# is not that order and differs between the install environment and the installed OS, so
|
||||
# a disk derived as /dev/sd<letter of its index> can be the wrong one. Resolve by target.
|
||||
- name: List disks with their SCSI address
|
||||
ansible.builtin.command: lsblk -dnpo NAME,TYPE,HCTL
|
||||
register: partitioning_vmware_lsblk
|
||||
changed_when: false
|
||||
|
||||
- name: Map SCSI targets to devices
|
||||
vars:
|
||||
_scsi: >-
|
||||
{%- set out = [] -%}
|
||||
{%- for line in partitioning_vmware_lsblk.stdout_lines -%}
|
||||
{%- set p = line.split() -%}
|
||||
{%- if (p | length) == 3 and p[1] == 'disk' and p[2] is match('^\d+:0:\d+:0$') -%}
|
||||
{%- set _ = out.append({'name': p[0], 'host': p[2].split(':')[0], 'target': p[2].split(':')[2]}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
ansible.builtin.set_fact:
|
||||
partitioning_vmware_hosts: "{{ _scsi | map(attribute='host') | unique | list }}"
|
||||
partitioning_vmware_targets: "{{ _scsi | items2dict(key_name='target', value_name='name') }}"
|
||||
partitioning_vmware_target_count: "{{ _scsi | length }}"
|
||||
|
||||
- name: Assert the system disks sit on one SCSI controller with unique targets
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- partitioning_vmware_hosts | length == 1
|
||||
- partitioning_vmware_targets | length == partitioning_vmware_target_count | int
|
||||
fail_msg: >-
|
||||
Expected every disk on one SCSI controller with unique targets, got
|
||||
{{ partitioning_vmware_lsblk.stdout_lines | select('search', ' disk ') | list }}.
|
||||
quiet: true
|
||||
|
||||
- name: Assert each system disk has a device at its SCSI target
|
||||
vars:
|
||||
_target: "{{ (idx if idx < 7 else idx + 1) | string }}"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- _target in partitioning_vmware_targets
|
||||
fail_msg: "system.disks[{{ idx }}] ({{ item.size }} GB) expects a disk at SCSI target {{ _target }}, none found."
|
||||
quiet: true
|
||||
loop: "{{ system_cfg.disks }}"
|
||||
loop_control:
|
||||
index_var: idx
|
||||
label: "{{ idx }}"
|
||||
|
||||
- name: Point system disks and install_drive at the resolved devices
|
||||
vars:
|
||||
_disks: >-
|
||||
{%- set out = [] -%}
|
||||
{%- for d in system_cfg.disks -%}
|
||||
{%- set dev = partitioning_vmware_targets[(loop.index0 if loop.index0 < 7 else loop.index0 + 1) | string] -%}
|
||||
{%- set _ = out.append(d | combine({'device': dev, 'partition': dev ~ '1'})) -%}
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
ansible.builtin.set_fact:
|
||||
system_cfg: "{{ system_cfg | combine({'disks': _disks}) }}"
|
||||
install_drive: "{{ _disks[0].device }}"
|
||||
@@ -1,10 +1,4 @@
|
||||
---
|
||||
- name: Resolve VMware disks by SCSI target
|
||||
when:
|
||||
- hypervisor_type == "vmware"
|
||||
- system_cfg.type == "virtual"
|
||||
ansible.builtin.include_tasks: _resolve_vmware_disks.yml
|
||||
|
||||
- name: Detect system sizing
|
||||
ansible.builtin.include_tasks: _detect_sizing.yml
|
||||
|
||||
|
||||
@@ -50,9 +50,6 @@
|
||||
esxi_hostname: "{{ hypervisor_cfg.node }}"
|
||||
register: system_check_esxi
|
||||
delegate_facts: true
|
||||
until: system_check_esxi.ansible_facts is defined
|
||||
retries: 3
|
||||
delay: 10
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
no_log: true
|
||||
@@ -87,9 +84,6 @@
|
||||
community.vmware.vmware_datastore_info:
|
||||
cluster: "{{ hypervisor_cfg.cluster }}"
|
||||
register: system_check_cluster
|
||||
until: system_check_cluster.datastores is defined
|
||||
retries: 3
|
||||
delay: 10
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
no_log: true
|
||||
@@ -138,10 +132,6 @@
|
||||
path: "{{ item.path }}"
|
||||
state: file
|
||||
register: system_check_media_files
|
||||
# A missing file fails with status 404; a dropped vCenter connection fails with status None.
|
||||
until: system_check_media_files is not failed or system_check_media_files.status is not none
|
||||
retries: 3
|
||||
delay: 10
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
loop: "{{ system_check_datastores_needed | rejectattr('source', 'equalto', 'hypervisor.storage') | list }}"
|
||||
|
||||
@@ -82,11 +82,6 @@
|
||||
name: "{{ hostname }}"
|
||||
folder: "{{ system_cfg.path if system_cfg.path | length > 0 else omit }}"
|
||||
register: system_check_vmware_check_result
|
||||
until: >-
|
||||
not system_check_vmware_check_result.failed
|
||||
or (system_check_vmware_check_result.msg is search('non-existing VM'))
|
||||
retries: 3
|
||||
delay: 10
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
{%- endfor -%}
|
||||
{{ out }}
|
||||
community.proxmox.proxmox_kvm:
|
||||
ciuser: "{{ bootstrap_primary_user.name }}"
|
||||
cipassword: "{{ bootstrap_primary_user.password }}"
|
||||
ciuser: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).key }}"
|
||||
cipassword: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).value.password }}"
|
||||
ciupgrade: false
|
||||
vmid: "{{ system_cfg.id }}"
|
||||
name: "{{ hostname }}"
|
||||
|
||||
@@ -12,9 +12,6 @@ network:
|
||||
{% if has_static %}
|
||||
addresses:
|
||||
- "{{ iface.ip }}/{{ iface.prefix }}"
|
||||
{% for address in iface.extra_addresses | default([]) %}
|
||||
- "{{ address }}"
|
||||
{% endfor %}
|
||||
{% if iface.gateway | default('') | string | length %}
|
||||
routes:
|
||||
- to: default
|
||||
|
||||
@@ -14,9 +14,8 @@ users:
|
||||
{% elif _sudo | bool %}
|
||||
sudo: "ALL=(ALL) NOPASSWD:ALL"
|
||||
{% endif %}
|
||||
{% set _pw = attrs.password | default('') %}
|
||||
{% if _pw | length > 0 %}
|
||||
passwd: "{{ _pw if (_pw | string)[:1] == '$' else _pw | password_hash('sha512') }}"
|
||||
{% if attrs.password | default('') | length > 0 %}
|
||||
passwd: "{{ attrs.password | password_hash('sha512') }}"
|
||||
lock_passwd: false
|
||||
{% else %}
|
||||
lock_passwd: true
|
||||
|
||||
@@ -34,4 +34,4 @@ merge_fixtures:
|
||||
disable: [nvidia, intel] # drop the nvidia GPU and every intel-vendor contribution
|
||||
expect_profile: { fingerprint: false, bluetooth: false }
|
||||
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
|
||||
os_family: Archlinux
|
||||
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]
|
||||
|
||||
- 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