refactor(users): migrate system.user to system.users[] for multi-user support

This commit is contained in:
2026-02-12 22:52:15 +01:00
parent 66057bc9b2
commit 8f8ce341ae
14 changed files with 186 additions and 139 deletions

View File

@@ -5,3 +5,14 @@
dest: /mnt/etc/sudoers.d/01-wheel
mode: "0440"
validate: /usr/sbin/visudo --check --file=%s
- name: Deploy per-user sudoers rules
when: item.sudo is defined and (item.sudo | string | length) > 0
ansible.builtin.copy:
content: "{{ item.name }} {{ item.sudo }}\n"
dest: "/mnt/etc/sudoers.d/{{ item.name }}"
mode: "0440"
validate: /usr/sbin/visudo --check --file=%s
loop: "{{ system_cfg.users }}"
loop_control:
label: "{{ item.name }}"

View File

@@ -1,38 +1,53 @@
---
- name: Create user account
- name: Set root password
vars:
configuration_root_cmd: >-
{{ chroot_command }} /usr/sbin/usermod --password
'{{ system_cfg.root.password | password_hash('sha512') }}' root --shell /bin/bash
ansible.builtin.command: "{{ configuration_root_cmd }}"
register: configuration_root_result
changed_when: configuration_root_result.rc == 0
- name: Create user accounts
vars:
configuration_user_group: >-
{{ "sudo" if is_debian | bool else "wheel" }}
configuration_useradd_cmd: >-
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group
--groups {{ configuration_user_group }} {{ system_cfg.user.name }}
--password {{ system_cfg.user.password | password_hash('sha512') }} --shell /bin/bash
configuration_root_cmd: >-
{{ chroot_command }} /usr/sbin/usermod --password
'{{ system_cfg.root.password | password_hash('sha512') }}' root --shell /bin/bash
ansible.builtin.command: "{{ item }}"
loop:
- "{{ configuration_useradd_cmd }}"
- "{{ configuration_root_cmd }}"
--uid {{ 1000 + ansible_loop.index0 }}
--groups {{ configuration_user_group }} {{ item.name }}
--password {{ item.password | password_hash('sha512') }} --shell /bin/bash
ansible.builtin.command: "{{ configuration_useradd_cmd }}"
loop: "{{ system_cfg.users }}"
loop_control:
extended: true
label: "{{ item.name }}"
register: configuration_user_result
changed_when: configuration_user_result.rc == 0
- name: Ensure .ssh directory exists
when: system_cfg.user.keys | length > 0
when: item.keys | default([]) | length > 0
ansible.builtin.file:
path: /mnt/home/{{ system_cfg.user.name }}/.ssh
path: "/mnt/home/{{ item.name }}/.ssh"
state: directory
owner: 1000
group: 1000
owner: "{{ 1000 + ansible_loop.index0 }}"
group: "{{ 1000 + ansible_loop.index0 }}"
mode: "0700"
loop: "{{ system_cfg.users }}"
loop_control:
extended: true
label: "{{ item.name }}"
- name: Add SSH public keys to authorized_keys
when: system_cfg.user.keys | length > 0
vars:
_uid: "{{ 1000 + (system_cfg.users | map(attribute='name') | list).index(item.0.name) }}"
ansible.builtin.lineinfile:
path: /mnt/home/{{ system_cfg.user.name }}/.ssh/authorized_keys
line: "{{ item }}"
owner: 1000
group: 1000
path: "/mnt/home/{{ item.0.name }}/.ssh/authorized_keys"
line: "{{ item.1 }}"
owner: "{{ _uid }}"
group: "{{ _uid }}"
mode: "0600"
create: true
loop: "{{ system_cfg.user.keys }}"
loop: "{{ system_cfg.users | subelements('keys', skip_missing=True) }}"
loop_control:
label: "{{ item.0.name }}: {{ item.1[:40] }}..."

View File

@@ -40,10 +40,7 @@ system_defaults:
path: ""
packages: []
disks: []
user:
name: ""
password: ""
keys: []
users: []
root:
password: ""
luks:

View File

@@ -66,9 +66,9 @@
- system_cfg.type == "virtual"
- hypervisor_type != "vmware"
ansible.builtin.set_fact:
ansible_user: "{{ system_cfg.user.name }}"
ansible_password: "{{ system_cfg.user.password }}"
ansible_become_password: "{{ system_cfg.user.password }}"
ansible_user: "{{ system_cfg.users[0].name }}"
ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
changed_when: false

View File

@@ -9,14 +9,14 @@
that:
- system is mapping
- system.network is not defined or system.network is mapping
- system.user is not defined or system.user is mapping
- system.users is not defined or (system.users is iterable and system.users is not string and system.users is not 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, user, root, luks, features) must be dictionaries."
fail_msg: "system and its nested keys (network, root, luks, features) must be dictionaries; system.users must be a list."
quiet: true
- name: Validate DNS and user.keys are lists (not strings)
- name: Validate DNS lists (not strings)
when: system.network is defined and system.network.dns is defined
ansible.builtin.assert:
that:
@@ -25,13 +25,18 @@
fail_msg: "system.network.dns.servers and system.network.dns.search must be lists, not strings."
quiet: true
- name: Validate user.keys is a list
when: system.user is defined and system.user.keys is defined
- name: Validate system.users entries
when: system.users is defined and system.users | length > 0
ansible.builtin.assert:
that:
- system.user.keys is iterable and system.user.keys is not string
fail_msg: "system.user.keys must be a list of SSH public key strings."
- item is mapping
- item.name is defined and (item.name | string | length) > 0
- item.keys is not defined or (item.keys is iterable and item.keys is not string)
fail_msg: "Each system.users[] entry must be a dict with 'name'; 'keys' must be a list."
quiet: true
loop: "{{ system.users }}"
loop_control:
label: "{{ item.name | default('(unnamed)') }}"
- name: Validate system features input types
when: system.features is defined
@@ -122,10 +127,7 @@
| list
}}
disks: "{{ system_raw.disks | default([]) }}"
user:
name: "{{ system_raw.user.name | string }}"
password: "{{ system_raw.user.password | string }}"
keys: "{{ system_raw.user.keys | default([]) }}"
users: "{{ system_raw.users | default([]) }}"
root:
password: "{{ system_raw.root.password | string }}"
luks:

View File

@@ -48,25 +48,9 @@
fail_msg: "Unsupported system keys: {{ system_unknown_keys | join(', ') }}."
quiet: true
- name: Validate nested system mappings
loop:
- network
- user
- root
- luks
- features
loop_control:
label: "{{ item }}"
ansible.builtin.assert:
that:
- system[item] is not defined or system[item] is mapping
fail_msg: "system.{{ item }} must be a dictionary."
quiet: true
- name: Validate system sub-dict schemas
loop:
- network
- user
- root
- luks
loop_control:

View File

@@ -34,8 +34,8 @@
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
ciuser: "{{ system_cfg.user.name }}"
cipassword: "{{ system_cfg.user.password }}"
ciuser: "{{ system_cfg.users[0].name }}"
cipassword: "{{ system_cfg.users[0].password }}"
ciupgrade: false
node: "{{ hypervisor_cfg.host }}"
vmid: "{{ system_cfg.id }}"

View File

@@ -4,9 +4,18 @@ ssh_pwauth: true
package_update: false
package_upgrade: false
users:
- name: "{{ system_cfg.user.name }}"
primary_group: "{{ system_cfg.user.name }}"
{% for user in system_cfg.users %}
- name: "{{ user.name }}"
primary_group: "{{ user.name }}"
groups: users
sudo: ALL=(ALL) NOPASSWD:ALL
passwd: "{{ system_cfg.user.password | password_hash('sha512') }}"
lock_passwd: False
sudo: "{{ user.sudo | default('ALL=(ALL) NOPASSWD:ALL') }}"
passwd: "{{ user.password | password_hash('sha512') }}"
lock_passwd: false
{% set ssh_keys = user.keys | default([]) %}
{% if ssh_keys | length > 0 %}
ssh_authorized_keys:
{% for key in ssh_keys %}
- "{{ key }}"
{% endfor %}
{% endif %}
{% endfor %}