fix(global_defaults): validate system.users as the documented dict on both paths
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
[defaults]
|
[defaults]
|
||||||
hash_behaviour = merge
|
hash_behaviour = merge
|
||||||
|
roles_path = roles
|
||||||
interpreter_python = auto_silent
|
interpreter_python = auto_silent
|
||||||
deprecation_warnings = False
|
deprecation_warnings = False
|
||||||
host_key_checking = False
|
host_key_checking = False
|
||||||
|
|||||||
@@ -8,11 +8,10 @@
|
|||||||
that:
|
that:
|
||||||
- system is mapping
|
- system is mapping
|
||||||
- system.network is not defined or system.network 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.root is not defined or system.root is mapping
|
||||||
- system.luks is not defined or system.luks is mapping
|
- system.luks is not defined or system.luks is mapping
|
||||||
- system.features is not defined or system.features 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."
|
fail_msg: "system and its nested keys (network, root, luks, features) must be dictionaries."
|
||||||
quiet: true
|
quiet: true
|
||||||
|
|
||||||
- name: Validate DNS lists (not strings)
|
- name: Validate DNS lists (not strings)
|
||||||
@@ -24,19 +23,6 @@
|
|||||||
fail_msg: "system.network.dns.servers and system.network.dns.search must be lists, not strings."
|
fail_msg: "system.network.dns.servers and system.network.dns.search must be lists, not strings."
|
||||||
quiet: true
|
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
|
- name: Validate system features input types
|
||||||
when: system.features is defined
|
when: system.features is defined
|
||||||
loop: "{{ system_defaults.features | dict2items | map(attribute='key') | list }}"
|
loop: "{{ system_defaults.features | dict2items | map(attribute='key') | list }}"
|
||||||
|
|||||||
@@ -65,6 +65,27 @@
|
|||||||
fail_msg: "Unsupported system.{{ item }} keys: {{ sub_unknown | join(', ') }}"
|
fail_msg: "Unsupported system.{{ item }} keys: {{ sub_unknown | join(', ') }}"
|
||||||
quiet: true
|
quiet: true
|
||||||
|
|
||||||
|
- name: Validate system.users is a dict keyed by username
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- system_cfg.users is mapping
|
||||||
|
fail_msg: >-
|
||||||
|
system.users must be a dict keyed by username, e.g. users: {alice: {password: ...,
|
||||||
|
keys: [...], sudo: true}}. A list of {name: ...} entries is not accepted.
|
||||||
|
quiet: true
|
||||||
|
|
||||||
|
- name: Validate system.users entries
|
||||||
|
loop: "{{ system_cfg.users | dict2items }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.key }}"
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- item.key | string | length > 0
|
||||||
|
- item.value is mapping
|
||||||
|
- ('keys' not in item.value) or (item.value['keys'] is iterable and item.value['keys'] is not string)
|
||||||
|
fail_msg: "system.users.{{ item.key }} must be a dict, and its keys a list of SSH public keys."
|
||||||
|
quiet: true
|
||||||
|
|
||||||
- name: Validate system.luks.tpm2 schema
|
- name: Validate system.luks.tpm2 schema
|
||||||
vars:
|
vars:
|
||||||
tpm2_input: >-
|
tpm2_input: >-
|
||||||
@@ -317,7 +338,7 @@
|
|||||||
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
|
_autologin: "{{ system_cfg.features.desktop.autologin | default(false) }}"
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- _autologin is boolean and not _autologin or (_autologin is string and _autologin | length > 0 and _autologin in system_cfg.users | map(attribute='name'))
|
- _autologin is boolean and not _autologin or (_autologin is string and _autologin | length > 0 and _autologin in system_cfg.users)
|
||||||
fail_msg: >-
|
fail_msg: >-
|
||||||
desktop.autologin must be false or a username string present in
|
desktop.autologin must be false or a username string present in
|
||||||
system.users; got '{{ _autologin }}'. Bool true is not accepted - the
|
system.users; got '{{ _autologin }}'. Bool true is not accepted - the
|
||||||
@@ -354,15 +375,13 @@
|
|||||||
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 | selectattr('password', 'defined') | map(attribute='password') | map('string') | reject('equalto', '') | list }}"
|
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- system_cfg.users | default([]) | length > 0
|
- bootstrap_primary_user | length > 0
|
||||||
- _pw_users | length > 0
|
fail_msg: >-
|
||||||
fail_msg: "At least one user with a non-empty password must be defined in system.users."
|
At least one user in system.users needs a non-empty password: bootstrap logs in
|
||||||
|
as the first such user after the reboot.
|
||||||
quiet: true
|
quiet: true
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Validate DNS servers is a list
|
- name: Validate DNS servers is a list
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
# 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"
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
---
|
||||||
|
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 must be a dict keyed by username
|
||||||
|
|
||||||
|
- name: list-rejected-precomputed
|
||||||
|
precomputed: true
|
||||||
|
system:
|
||||||
|
users:
|
||||||
|
- name: ops
|
||||||
|
password: pw
|
||||||
|
expect:
|
||||||
|
fail: system.users must be a dict keyed by username
|
||||||
|
|
||||||
|
- name: scalar-entry-rejected
|
||||||
|
system:
|
||||||
|
users:
|
||||||
|
ops: pw
|
||||||
|
expect:
|
||||||
|
fail: system.users.ops must be a dict
|
||||||
|
|
||||||
|
- name: scalar-entry-rejected-precomputed
|
||||||
|
precomputed: true
|
||||||
|
system:
|
||||||
|
users:
|
||||||
|
ops: pw
|
||||||
|
expect:
|
||||||
|
fail: system.users.ops must be a dict
|
||||||
|
|
||||||
|
- name: keys-string-rejected
|
||||||
|
system:
|
||||||
|
users:
|
||||||
|
ops:
|
||||||
|
password: pw
|
||||||
|
keys: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFixture ops
|
||||||
|
expect:
|
||||||
|
fail: its keys a list of SSH public keys
|
||||||
|
|
||||||
|
- 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