refactor(users): change system.users from list to dict keyed by username

This commit is contained in:
MORAWSKI Norbert
2026-03-20 14:33:13 +01:00
parent 398f1b081d
commit c0e672a32a
9 changed files with 73 additions and 83 deletions

View File

@@ -39,33 +39,24 @@
no_log: true no_log: true
vars: vars:
system_input: "{{ system | default({}) }}" system_input: "{{ system | default({}) }}"
system_users_input: "{{ system_input.users | default([]) }}" system_users_input: "{{ system_input.users | default({}) }}"
system_first_user: >- _first_entry: "{{ system_users_input | dict2items | first | default({'key': '', 'value': {}}) }}"
{{ _first_name: "{{ _first_entry.key }}"
system_users_input[0] _first_attrs: "{{ _first_entry.value if _first_entry.value is mapping else {} }}"
if (system_users_input is iterable and system_users_input is not string
and system_users_input is not mapping and system_users_input | length > 0)
else {}
}}
system_root_input: "{{ (system_input.root | default({})) if (system_input.root is mapping) else {} }}" system_root_input: "{{ (system_input.root | default({})) if (system_input.root is mapping) else {} }}"
prompt_user_name: "{{ user_name | default(system_user_name | default(''), true) | string }}" prompt_user_name: "{{ user_name | default(system_user_name | default(''), true) | string }}"
prompt_user_key: "{{ user_public_key | default(user_key | default(system_user_key | default(''), true), true) | string | trim }}" prompt_user_key: "{{ user_public_key | default(user_key | default(system_user_key | default(''), true), true) | string | trim }}"
prompt_user_password: "{{ user_password | default(system_user_password | default(''), true) | string }}" prompt_user_password: "{{ user_password | default(system_user_password | default(''), true) | string }}"
prompt_root_password: "{{ root_password | default(system_root_password | default(''), true) | string }}" prompt_root_password: "{{ root_password | default(system_root_password | default(''), true) | string }}"
resolved_user: resolved_name: "{{ _first_name if (_first_name | length > 0) else prompt_user_name }}"
name: >- resolved_attrs:
{{
system_first_user.name | string
if (system_first_user.name | default('') | string | length) > 0
else prompt_user_name
}}
keys: >- keys: >-
{{ {{
system_first_user['keys'] _first_attrs['keys']
if (system_first_user['keys'] is defined if (_first_attrs['keys'] is defined
and system_first_user['keys'] is iterable and _first_attrs['keys'] is iterable
and system_first_user['keys'] is not string and _first_attrs['keys'] is not string
and system_first_user['keys'] | length > 0) and _first_attrs['keys'] | length > 0)
else ( else (
[prompt_user_key] [prompt_user_key]
if (prompt_user_key | length > 0) if (prompt_user_key | length > 0)
@@ -74,8 +65,8 @@
}} }}
password: >- password: >-
{{ {{
system_first_user.password | string _first_attrs.password | string
if (system_first_user.password | default('') | string | length) > 0 if (_first_attrs.password | default('') | string | length) > 0
else prompt_user_password else prompt_user_password
}} }}
ansible.builtin.set_fact: ansible.builtin.set_fact:
@@ -84,14 +75,7 @@
system_input system_input
| combine( | combine(
{ {
'users': ( 'users': system_users_input | combine({resolved_name: (_first_attrs | combine(resolved_attrs, recursive=True))}),
[resolved_user]
+ (system_users_input[1:]
if (system_users_input is sequence
and system_users_input is not string
and system_users_input | length > 1)
else [])
),
'root': { 'root': {
'password': ( 'password': (
(system_root_input.password | default('') | string | length) > 0 (system_root_input.password | default('') | string | length) > 0
@@ -206,10 +190,12 @@
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_user: "{{ system_cfg.users[0].name }}" ansible_user: "{{ _primary.key }}"
ansible_password: "{{ system_cfg.users[0].password }}" ansible_password: "{{ _primary.value.password }}"
ansible_become_password: "{{ system_cfg.users[0].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

View File

@@ -15,15 +15,15 @@
validate: /usr/sbin/visudo --check --file=%s validate: /usr/sbin/visudo --check --file=%s
- name: Deploy per-user sudoers rules - name: Deploy per-user sudoers rules
when: item.sudo | default(false) when: item.value.sudo | default(false)
vars: vars:
configuration_sudoers_rule: >- configuration_sudoers_rule: >-
{{ item.sudo if item.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }} {{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
ansible.builtin.copy: ansible.builtin.copy:
content: "{{ item.name }} {{ configuration_sudoers_rule }}\n" content: "{{ item.key }} {{ configuration_sudoers_rule }}\n"
dest: "/mnt/etc/sudoers.d/{{ item.name }}" dest: "/mnt/etc/sudoers.d/{{ item.key }}"
mode: "0440" mode: "0440"
validate: /usr/sbin/visudo --check --file=%s validate: /usr/sbin/visudo --check --file=%s
loop: "{{ system_cfg.users }}" loop: "{{ system_cfg.users | dict2items }}"
loop_control: loop_control:
label: "{{ item.name }}" label: "{{ item.key }}"

View File

@@ -26,44 +26,43 @@
- name: Create user accounts - name: Create user accounts
vars: vars:
configuration_user_group: "{{ _configuration_platform.user_group }}" configuration_user_group: "{{ _configuration_platform.user_group }}"
# UID starts at 1000; safe for fresh installs only
configuration_useradd_cmd: >- configuration_useradd_cmd: >-
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group {{ chroot_command }} /usr/sbin/useradd --create-home --user-group
--uid {{ 1000 + ansible_loop.index0 }} --uid {{ 1000 + _idx }}
--groups {{ configuration_user_group }} {{ item.name }} --groups {{ configuration_user_group }} {{ item.key }}
--password {{ item.password | password_hash('sha512') }} --shell {{ item.shell | default('/bin/bash') }} {{ ('--password ' ~ (item.value.password | password_hash('sha512'))) if (item.value.password | default('') | string | length > 0) else '' }}
--shell {{ item.value.shell | default('/bin/bash') }}
ansible.builtin.command: "{{ configuration_useradd_cmd }}" ansible.builtin.command: "{{ configuration_useradd_cmd }}"
loop: "{{ system_cfg.users }}" loop: "{{ system_cfg.users | dict2items }}"
loop_control: loop_control:
extended: true index_var: _idx
label: "{{ item.name }}" label: "{{ item.key }}"
register: configuration_user_result register: configuration_user_result
changed_when: configuration_user_result.rc == 0 changed_when: configuration_user_result.rc == 0
no_log: true no_log: true
- name: Ensure .ssh directory exists - name: Ensure .ssh directory exists
when: "'keys' in item and item['keys'] is iterable and item['keys'] is not string and item['keys'] | length > 0" when: (item.value['keys'] | default([]) | length) > 0
ansible.builtin.file: ansible.builtin.file:
path: "/mnt/home/{{ item.name }}/.ssh" path: "/mnt/home/{{ item.key }}/.ssh"
state: directory state: directory
owner: "{{ 1000 + ansible_loop.index0 }}" owner: "{{ 1000 + _idx }}"
group: "{{ 1000 + ansible_loop.index0 }}" group: "{{ 1000 + _idx }}"
mode: "0700" mode: "0700"
loop: "{{ system_cfg.users }}" loop: "{{ system_cfg.users | dict2items }}"
loop_control: loop_control:
extended: true index_var: _idx
label: "{{ item.name }}" label: "{{ item.key }}"
- name: Add SSH public keys to authorized_keys - name: Deploy SSH authorized_keys
vars: when: (item.value['keys'] | default([]) | length) > 0
configuration_uid: "{{ 1000 + (system_cfg.users | map(attribute='name') | list).index(item.0.name) }}" ansible.builtin.copy:
ansible.builtin.lineinfile: content: "{{ item.value['keys'] | join('\n') }}\n"
path: "/mnt/home/{{ item.0.name }}/.ssh/authorized_keys" dest: "/mnt/home/{{ item.key }}/.ssh/authorized_keys"
line: "{{ item.1 }}" owner: "{{ 1000 + _idx }}"
owner: "{{ configuration_uid }}" group: "{{ 1000 + _idx }}"
group: "{{ configuration_uid }}"
mode: "0600" mode: "0600"
create: true loop: "{{ system_cfg.users | dict2items }}"
loop: "{{ system_cfg.users | subelements('keys', skip_missing=True) }}"
loop_control: loop_control:
label: "{{ item.0.name }}: {{ item.1[:40] }}..." index_var: _idx
label: "{{ item.key }}"

View File

@@ -85,7 +85,7 @@ system_defaults:
mirror: "" mirror: ""
packages: [] packages: []
disks: [] disks: []
users: [] users: {}
root: root:
password: "" password: ""
shell: "/bin/bash" shell: "/bin/bash"

View File

@@ -96,7 +96,7 @@
}} }}
# --- Storage & accounts --- # --- Storage & accounts ---
disks: "{{ system_raw.disks | default([]) }}" disks: "{{ system_raw.disks | default([]) }}"
users: "{{ system_raw.users | default([]) }}" users: "{{ system_raw.users | default({}) }}"
root: root:
password: "{{ system_raw.root.password | string }}" password: "{{ system_raw.root.password | string }}"
shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}" shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}"

View File

@@ -25,17 +25,17 @@
quiet: true quiet: true
- name: Validate system.users entries - name: Validate system.users entries
when: system.users is defined and system.users | length > 0 when: system.users is defined and system.users is mapping and system.users | length > 0
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- item is mapping - item.value is mapping
- item.name is defined and (item.name | string | length) > 0 - item.key | string | length > 0
- item['keys'] is not defined or (item['keys'] is iterable and item['keys'] is not string) - item.value['keys'] is not defined or (item.value['keys'] is iterable and item.value['keys'] is not string)
fail_msg: "Each system.users[] entry must be a dict with 'name'; 'keys' must be a list." fail_msg: "Each system.users entry must be a dict keyed by username; 'keys' must be a list."
quiet: true quiet: true
loop: "{{ system.users }}" loop: "{{ system.users | dict2items }}"
loop_control: loop_control:
label: "{{ item.name | default('(unnamed)') }}" 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

View File

@@ -81,10 +81,12 @@
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_user: "{{ system_cfg.users[0].name }}" ansible_user: "{{ _primary.key }}"
ansible_password: "{{ system_cfg.users[0].password }}" ansible_password: "{{ _primary.value.password }}"
ansible_become_password: "{{ system_cfg.users[0].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

View File

@@ -261,13 +261,16 @@
fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size." fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size."
quiet: true quiet: true
- name: Validate at least one user 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:
- system_cfg.users | default([]) | length > 0 - system_cfg.users | default({}) | length > 0
- system_cfg.users[0].name is defined and (system_cfg.users[0].name | string | length) > 0 - _pw_users | length > 0
- system_cfg.users[0].password is defined and (system_cfg.users[0].password | string | length) > 0 - _pw_users[0].key | string | length > 0
fail_msg: "At least one user with a name and password must be defined in system.users[]." - _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 quiet: true
no_log: true no_log: true

View File

@@ -35,8 +35,8 @@
{%- endfor -%} {%- endfor -%}
{{ out }} {{ out }}
community.proxmox.proxmox_kvm: community.proxmox.proxmox_kvm:
ciuser: "{{ system_cfg.users[0].name }}" ciuser: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).key }}"
cipassword: "{{ system_cfg.users[0].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 }}"