Compare commits

...

24 Commits

Author SHA1 Message Date
a6bc7ffe04 fix(configuration): use /boot/grub2/grub.cfg for RedHat EFI grub config 2026-03-25 16:03:39 +01:00
c529e71ebc feat(packages): add needrestart to Debian and Ubuntu package lists 2026-03-20 18:06:14 +01:00
cb46de2b6d feat(bootstrap): add full package upgrade step for Debian and Ubuntu 2026-03-20 18:05:04 +01:00
9169117b25 fix(vim): use vimscript comment syntax for blockinfile markers in vimrc 2026-03-20 18:00:12 +01:00
6c94c519fb fix(sudo): use explicit string check instead of bool conditional for sudo field 2026-03-20 17:31:49 +01:00
efd96a42b8 fix(connection): set ansible_port explicitly at every connection transition 2026-03-20 17:31:49 +01:00
68661c3cca fix(vmware): use primary ansible_* vars for vmware_tools connection plugin precedence 2026-03-20 17:31:49 +01:00
1db20c7ac0 fix(vmware): use empty password for vmware_tools during live ISO bootstrap 2026-03-20 17:31:49 +01:00
7b155b427b fix(users): update cloud-init template and input validation for dict users 2026-03-20 17:31:49 +01:00
ca8721e98f refactor(prompts): remove vars_prompt, require users defined in inventory 2026-03-20 17:31:49 +01:00
cdb2559d8f fix(prompts): add default values to vars_prompt to skip in non-interactive mode 2026-03-20 17:31:49 +01:00
443f6623df refactor(users): change system.users from list to dict keyed by username 2026-03-20 17:31:49 +01:00
6cf418fe00 fix(configuration): make root password, user keys, and sudo all optional 2026-03-20 17:31:49 +01:00
47ec5fe621 fix(cloud-init): handle missing keys and make sudo conditional 2026-03-20 17:31:49 +01:00
240f945cce fix(cleanup): remove ansible_become override that blocks swapoff/umount 2026-03-20 17:31:49 +01:00
663a04556f feat(global_defaults): add system.features.aur schema for validation passthrough 2026-03-20 17:31:49 +01:00
6febd1acf1 refactor(virtualization): extract shared Xen disk definitions 2026-03-12 12:27:18 +01:00
008187860c refactor: remove unnecessary changed_when from set_fact tasks 2026-03-12 12:25:45 +01:00
cd1be6b5e1 refactor(partitioning): remove redundant blockdev --rereadpt calls 2026-03-12 12:25:15 +01:00
15be6149fd refactor(partitioning): remove unused register variables 2026-03-12 12:24:59 +01:00
ca29ad200d chore: suppress args[module] false positives from variable-based module_defaults 2026-03-12 12:12:27 +01:00
8079099cee fix(cleanup): add no_log to Proxmox VM restart task 2026-03-12 12:12:27 +01:00
9e79185b07 fix(virtualization): add missing changed_when to Xen VM stop task 2026-03-12 12:12:27 +01:00
b88bf2860f fix(configuration): replace fail+ignore_errors with debug for TPM2 fallback warning 2026-03-12 12:12:27 +01:00
30 changed files with 162 additions and 231 deletions

View File

@@ -1,5 +1,6 @@
skip_list: skip_list:
- run-once - run-once
- var-naming[no-role-prefix] # user-facing API dicts (cis, system, hypervisor) are intentionally not role-prefixed - var-naming[no-role-prefix] # user-facing API dicts (cis, system, hypervisor) are intentionally not role-prefixed
- args[module] # false positives from variable-based module_defaults (_proxmox_auth, _vmware_auth)
exclude_paths: exclude_paths:
- roles/global_defaults/ - roles/global_defaults/

View File

@@ -202,14 +202,29 @@ When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`
#### `system.users` #### `system.users`
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:
users:
svcansible:
password: "vault_lookup"
keys:
- "ssh-ed25519 AAAA..."
appuser:
sudo: "ALL=(ALL) NOPASSWD: ALL"
keys:
- "ssh-ed25519 BBBB..."
```
| Key | Type | Default | Description | | Key | Type | Default | Description |
| ---------- | ----------- | ------- | -------------------------------------------------- | | ---------- | ----------- | ------- | -------------------------------------------------- |
| `name` | string | -- | Username (required) | | *(dict key)* | string | -- | Username (required) |
| `password` | string | -- | User password (required for first user) | | `password` | string | -- | User password (required for at least one user) |
| `keys` | list | `[]` | SSH public keys | | `keys` | list | `[]` | SSH public keys |
| `sudo` | bool/string | -- | `true` for NOPASSWD ALL, or custom sudoers string | | `sudo` | bool/string | -- | `true` for NOPASSWD ALL, or custom sudoers string |
The first user's credentials are prompted interactively via `vars_prompt` unless supplied in inventory or `-e`. Users must be defined in inventory. The dict format enables additive merging across inventory layers with `hash_behaviour=merge`.
#### `system.root` #### `system.root`
@@ -398,7 +413,7 @@ ansible-playbook -i inventory.yml main.yml
ansible-playbook -i inventory.yml main.yml -e @vars.yml ansible-playbook -i inventory.yml main.yml -e @vars.yml
``` ```
Credentials for the first user and root are prompted interactively via `vars_prompt` unless already set in inventory or passed via `-e`. All credentials (`system.users`, `system.root.password`) must be defined in inventory or passed via `-e`.
Example inventory files are included: Example inventory files are included:
@@ -408,7 +423,7 @@ Example inventory files are included:
## 7. Security ## 7. Security
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, `system.users[].password`, `system.root.password`). Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, user passwords in `system.users`, `system.root.password`).
## 8. Safety ## 8. Safety

100
main.yml
View File

@@ -14,94 +14,7 @@
strategy: free # noqa: run-once[play] strategy: free # noqa: run-once[play]
gather_facts: false gather_facts: false
become: true become: true
vars_prompt:
- name: user_name
prompt: |
What is your username?
private: false
- name: user_public_key
prompt: |
What is your ssh key?
private: false
- name: user_password
prompt: |
What is your password?
confirm: true
- name: root_password
prompt: |
What is your root password?
confirm: true
pre_tasks: pre_tasks:
- name: Apply prompted authentication values to system input
no_log: true
vars:
system_input: "{{ system | default({}) }}"
system_users_input: "{{ system_input.users | default([]) }}"
system_first_user: >-
{{
system_users_input[0]
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 {} }}"
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_password: "{{ user_password | default(system_user_password | default(''), true) | string }}"
prompt_root_password: "{{ root_password | default(system_root_password | default(''), true) | string }}"
resolved_user:
name: >-
{{
system_first_user.name | string
if (system_first_user.name | default('') | string | length) > 0
else prompt_user_name
}}
keys: >-
{{
system_first_user['keys']
if (system_first_user['keys'] is defined
and system_first_user['keys'] is iterable
and system_first_user['keys'] is not string
and system_first_user['keys'] | length > 0)
else (
[prompt_user_key]
if (prompt_user_key | length > 0)
else []
)
}}
password: >-
{{
system_first_user.password | string
if (system_first_user.password | default('') | string | length) > 0
else prompt_user_password
}}
ansible.builtin.set_fact:
system: >-
{{
system_input
| combine(
{
'users': (
[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': {
'password': (
(system_root_input.password | default('') | string | length) > 0
) | ternary(system_root_input.password | string, prompt_root_password)
}
},
recursive=True
)
}}
- name: Load global defaults - name: Load global defaults
ansible.builtin.import_role: ansible.builtin.import_role:
name: global_defaults name: global_defaults
@@ -160,8 +73,6 @@
ansible.builtin.include_role: ansible.builtin.include_role:
name: cleanup name: cleanup
public: true public: true
vars:
ansible_become: false
rescue: rescue:
- name: Delete VM on bootstrap failure - name: Delete VM on bootstrap failure
@@ -208,10 +119,15 @@
when: when:
- post_reboot_can_connect | bool - post_reboot_can_connect | bool
no_log: true no_log: true
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
ansible_user: "{{ system_cfg.users[0].name }}" ansible_connection: ssh
ansible_password: "{{ system_cfg.users[0].password }}" ansible_host: "{{ system_cfg.network.ip }}"
ansible_become_password: "{{ system_cfg.users[0].password }}" ansible_port: 22
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_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
ansible_python_interpreter: /usr/bin/python3 ansible_python_interpreter: /usr/bin/python3

View File

@@ -55,6 +55,11 @@
register: bootstrap_debian_update_result register: bootstrap_debian_update_result
changed_when: bootstrap_debian_update_result.rc == 0 changed_when: bootstrap_debian_update_result.rc == 0
- name: Upgrade all packages to latest versions
ansible.builtin.command: "{{ chroot_command }} apt full-upgrade -y"
register: bootstrap_debian_upgrade_result
changed_when: "'0 upgraded' not in bootstrap_debian_upgrade_result.stdout"
- name: Install extra packages - name: Install extra packages
when: bootstrap_debian_extra_args | trim | length > 0 when: bootstrap_debian_extra_args | trim | length > 0
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra_args }}" ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra_args }}"

View File

@@ -54,6 +54,11 @@
register: bootstrap_ubuntu_update_result register: bootstrap_ubuntu_update_result
changed_when: bootstrap_ubuntu_update_result.rc == 0 changed_when: bootstrap_ubuntu_update_result.rc == 0
- name: Upgrade all packages to latest versions
ansible.builtin.command: "{{ chroot_command }} apt full-upgrade -y"
register: bootstrap_ubuntu_upgrade_result
changed_when: "'0 upgraded' not in bootstrap_ubuntu_upgrade_result.stdout"
- name: Install extra packages - name: Install extra packages
when: bootstrap_ubuntu_extra_args | trim | length > 0 when: bootstrap_ubuntu_extra_args | trim | length > 0
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra_args }}" ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra_args }}"

View File

@@ -201,6 +201,7 @@ bootstrap_debian:
- lrzsz - lrzsz
- mtr - mtr
- ncdu - ncdu
- needrestart
- net-tools - net-tools
- network-manager - network-manager
- python-is-python3 - python-is-python3
@@ -262,6 +263,7 @@ bootstrap_ubuntu:
- mtr - mtr
- ncdu - ncdu
- ncurses-term - ncurses-term
- needrestart
- net-tools - net-tools
- network-manager - network-manager
- python-is-python3 - python-is-python3

View File

@@ -14,7 +14,6 @@
- name: Initialize cleaned VM XML - name: Initialize cleaned VM XML
ansible.builtin.set_fact: ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_get_xml.get_xml }}" cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_get_xml.get_xml }}"
changed_when: false
- name: Remove boot ISO device from VM XML (source match) - name: Remove boot ISO device from VM XML (source match)
when: boot_iso is defined and boot_iso | length > 0 when: boot_iso is defined and boot_iso | length > 0
@@ -28,7 +27,6 @@
when: boot_iso is defined and boot_iso | length > 0 when: boot_iso is defined and boot_iso | length > 0
ansible.builtin.set_fact: ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot_source.xmlstring }}" cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot_source.xmlstring }}"
changed_when: false
- name: Remove boot ISO device from VM XML (target fallback) - name: Remove boot ISO device from VM XML (target fallback)
community.general.xml: community.general.xml:
@@ -40,7 +38,6 @@
- name: Update cleaned VM XML after removing boot ISO - name: Update cleaned VM XML after removing boot ISO
ansible.builtin.set_fact: ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot.xmlstring }}" cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot.xmlstring }}"
changed_when: false
- name: Remove cloud-init ISO device from VM XML (source match) - name: Remove cloud-init ISO device from VM XML (source match)
community.general.xml: community.general.xml:
@@ -52,7 +49,6 @@
- name: Update cleaned VM XML after removing cloud-init ISO source match - name: Update cleaned VM XML after removing cloud-init ISO source match
ansible.builtin.set_fact: ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit_source.xmlstring }}" cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit_source.xmlstring }}"
changed_when: false
- name: Remove cloud-init ISO device from VM XML (target fallback) - name: Remove cloud-init ISO device from VM XML (target fallback)
community.general.xml: community.general.xml:
@@ -64,7 +60,6 @@
- name: Update cleaned VM XML after removing cloud-init ISO - name: Update cleaned VM XML after removing cloud-init ISO
ansible.builtin.set_fact: ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}" cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}"
changed_when: false
- name: Strip XML declaration for libvirt define - name: Strip XML declaration for libvirt define
ansible.builtin.set_fact: ansible.builtin.set_fact:
@@ -76,7 +71,6 @@
| regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "") | regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "")
| trim | trim
}} }}
changed_when: false
- name: Update VM definition without installer media - name: Update VM definition without installer media
community.libvirt.virt: community.libvirt.virt:

View File

@@ -25,3 +25,4 @@
community.proxmox.proxmox_kvm: community.proxmox.proxmox_kvm:
vmid: "{{ system_cfg.id }}" vmid: "{{ system_cfg.id }}"
state: restarted state: restarted
no_log: true

View File

@@ -7,34 +7,11 @@
xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}" xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}"
block: block:
- name: Ensure Xen disk definitions exist - name: Ensure Xen disk definitions exist
when: virtualization_xen_disks is not defined ansible.builtin.include_tasks: ../../virtualization/tasks/_xen_disks.yml
ansible.builtin.set_fact:
cleanup_xen_disks: "{{ cleanup_xen_disks | default([]) + [cleanup_xen_disk_cfg] }}"
vars:
device_letter_map: "{{ disk_letter_map }}"
device_letter: "{{ device_letter_map[ansible_loop.index0] }}"
cleanup_xen_disk_cfg: >-
{{
{
'path': (
virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2'
if ansible_loop.index0 == 0
else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'
),
'target': 'xvd' ~ device_letter,
'size': (item.size | float)
}
}}
loop: "{{ system_cfg.disks }}"
loop_control:
label: "{{ item | to_json }}"
extended: true
changed_when: false
- name: Render Xen VM configuration without installer media - name: Render Xen VM configuration without installer media
vars: vars:
xen_installer_media_enabled: false xen_installer_media_enabled: false
virtualization_xen_disks: "{{ virtualization_xen_disks | default(cleanup_xen_disks | default([])) }}"
ansible.builtin.template: ansible.builtin.template:
src: xen.cfg.j2 src: xen.cfg.j2
dest: /tmp/xen-{{ hostname }}.cfg dest: /tmp/xen-{{ hostname }}.cfg

View File

@@ -52,9 +52,7 @@
vars: vars:
configuration_grub_cfg_cmd: >- configuration_grub_cfg_cmd: >-
{{ {{
'/usr/sbin/' + _configuration_platform.grub_mkconfig_prefix + ' -o ' '/usr/sbin/' + _configuration_platform.grub_mkconfig_prefix + ' -o /boot/grub2/grub.cfg'
+ partitioning_efi_mountpoint
+ '/EFI/' + _efi_vendor + '/grub.cfg'
if os_family == 'RedHat' if os_family == 'RedHat'
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg' else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
}} }}

View File

@@ -86,7 +86,6 @@
device: "{{ configuration_luks_device }}" device: "{{ configuration_luks_device }}"
passphrase: "{{ configuration_luks_passphrase }}" passphrase: "{{ configuration_luks_passphrase }}"
new_keyfile: "/mnt{{ configuration_luks_keyfile_path }}" new_keyfile: "/mnt{{ configuration_luks_keyfile_path }}"
register: configuration_luks_addkey_retry
failed_when: false failed_when: false
no_log: true no_log: true

View File

@@ -80,11 +80,10 @@
host stderr={{ configuration_luks_tpm2_enroll_host.stderr | default('') }} host stderr={{ configuration_luks_tpm2_enroll_host.stderr | default('') }}
rescue: rescue:
- name: Warn about TPM2 enrollment failure - name: Warn about TPM2 enrollment failure
ansible.builtin.fail: ansible.builtin.debug:
msg: >- msg: >-
WARNING: TPM2 enrollment failed — falling back to keyfile auto-decrypt. WARNING: TPM2 enrollment failed — falling back to keyfile auto-decrypt.
The system will use a keyfile instead of TPM2 for automatic LUKS unlock. The system will use a keyfile instead of TPM2 for automatic LUKS unlock.
ignore_errors: true
- name: Fallback to keyfile auto-decrypt - name: Fallback to keyfile auto-decrypt
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@@ -9,7 +9,7 @@
set smartindent set smartindent
set mouse=a set mouse=a
insertafter: EOF insertafter: EOF
marker: "# {mark} CUSTOM VIM CONFIG" marker: "\" {mark} CUSTOM VIM CONFIG"
failed_when: false failed_when: false
# Tuned for VM workloads: low swappiness, aggressive writeback, large page-cluster # Tuned for VM workloads: low swappiness, aggressive writeback, large page-cluster

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 is defined and (item.value.sudo | string | length > 0)
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

@@ -1,5 +1,6 @@
--- ---
- name: Set root password - name: Set root password
when: (system_cfg.root.password | default('') | string | length) > 0
ansible.builtin.shell: >- ansible.builtin.shell: >-
set -o pipefail && set -o pipefail &&
echo 'root:{{ system_cfg.root.password | password_hash("sha512") }}' | {{ chroot_command }} /usr/sbin/chpasswd -e echo 'root:{{ system_cfg.root.password | password_hash("sha512") }}' | {{ chroot_command }} /usr/sbin/chpasswd -e
@@ -9,6 +10,13 @@
changed_when: configuration_root_result.rc == 0 changed_when: configuration_root_result.rc == 0
no_log: true no_log: true
- name: Lock root account when no password is set
when: (system_cfg.root.password | default('') | string | length) == 0
ansible.builtin.command: >-
{{ chroot_command }} /usr/bin/passwd -l root
register: configuration_root_lock_result
changed_when: configuration_root_lock_result.rc == 0
- name: Set root shell - name: Set root shell
ansible.builtin.command: >- ansible.builtin.command: >-
{{ chroot_command }} /usr/sbin/usermod --shell {{ system_cfg.root.shell }} root {{ chroot_command }} /usr/sbin/usermod --shell {{ system_cfg.root.shell }} root
@@ -18,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: item['keys'] | default([]) | 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

@@ -87,9 +87,10 @@
- name: Switch to SSH connection - name: Switch to SSH connection
ansible.builtin.set_fact: ansible.builtin.set_fact:
ansible_connection: ssh ansible_connection: ssh
ansible_host: "{{ system_cfg.network.ip }}"
ansible_port: 22
ansible_user: root ansible_user: root
ansible_password: "" ansible_password: ""
ansible_host: "{{ system_cfg.network.ip }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
- name: Reset connection for SSH switchover - name: Reset connection for SSH switchover

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"
@@ -129,6 +129,10 @@ system_defaults:
rhel_repo: rhel_repo:
source: "iso" # iso|satellite|none — how RHEL systems get packages post-install source: "iso" # iso|satellite|none — how RHEL systems get packages post-install
url: "" # Satellite/custom repo URL when source=satellite url: "" # Satellite/custom repo URL when source=satellite
aur:
enabled: false
helper: "yay" # yay|paru
user: "_aur_builder"
chroot: chroot:
tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn

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

@@ -8,11 +8,11 @@
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 iterable and system.users is not string and system.users is not 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) must be dictionaries; system.users must be a list." fail_msg: "system and its nested keys (network, root, luks, features, users) must be dictionaries."
quiet: true quiet: true
- name: Validate DNS lists (not strings) - name: Validate DNS lists (not strings)
@@ -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,14 @@
when: when:
- system_cfg.type == "virtual" - system_cfg.type == "virtual"
- hypervisor_type != "vmware" - hypervisor_type != "vmware"
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
ansible_user: "{{ system_cfg.users[0].name }}" ansible_host: "{{ system_cfg.network.ip }}"
ansible_password: "{{ system_cfg.users[0].password }}" ansible_port: 22
ansible_become_password: "{{ system_cfg.users[0].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_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
no_log: true no_log: true
@@ -92,12 +96,12 @@
when: hypervisor_type == "vmware" when: hypervisor_type == "vmware"
ansible.builtin.set_fact: ansible.builtin.set_fact:
ansible_connection: vmware_tools ansible_connection: vmware_tools
ansible_vmware_host: "{{ hypervisor_cfg.url }}" ansible_host: "{{ hypervisor_cfg.url }}"
ansible_vmware_port: 443 ansible_port: 443
ansible_user: root
ansible_password: ""
ansible_vmware_user: "{{ hypervisor_cfg.username }}" ansible_vmware_user: "{{ hypervisor_cfg.username }}"
ansible_vmware_password: "{{ hypervisor_cfg.password }}" ansible_vmware_password: "{{ hypervisor_cfg.password }}"
ansible_vmware_guest_path: "/{{ hypervisor_cfg.datacenter }}/vm{{ system_cfg.path }}/{{ hostname }}" ansible_vmware_guest_path: "/{{ hypervisor_cfg.datacenter }}/vm{{ system_cfg.path }}/{{ hostname }}"
ansible_vmware_validate_certs: "{{ hypervisor_cfg.certs | bool }}" ansible_vmware_validate_certs: "{{ hypervisor_cfg.certs | bool }}"
ansible_vmware_tools_user: root
ansible_vmware_tools_password: "{{ system_cfg.root.password }}"
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

@@ -24,7 +24,6 @@
ansible.builtin.command: >- ansible.builtin.command: >-
tune2fs -O "^orphan_file,^metadata_csum_seed" tune2fs -O "^orphan_file,^metadata_csum_seed"
"{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}" "{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
register: partitioning_boot_ext4_tune_result
changed_when: false changed_when: false
- name: Create swap filesystem - name: Create swap filesystem

View File

@@ -65,9 +65,7 @@
ansible.builtin.command: "{{ item }}" ansible.builtin.command: "{{ item }}"
loop: loop:
- "partprobe {{ install_drive }}" - "partprobe {{ install_drive }}"
- "blockdev --rereadpt {{ install_drive }}"
- "udevadm settle" - "udevadm settle"
register: partitioning_partprobe_result
changed_when: false changed_when: false
failed_when: false failed_when: false
@@ -91,9 +89,7 @@
ansible.builtin.command: "{{ item }}" ansible.builtin.command: "{{ item }}"
loop: loop:
- "partprobe {{ install_drive }}" - "partprobe {{ install_drive }}"
- "blockdev --rereadpt {{ install_drive }}"
- "udevadm settle" - "udevadm settle"
register: partitioning_partprobe_retry
changed_when: false changed_when: false
failed_when: false failed_when: false
@@ -116,6 +112,5 @@
loop: loop:
- "partprobe {{ install_drive }}" - "partprobe {{ install_drive }}"
- "udevadm settle" - "udevadm settle"
register: partitioning_partprobe_settle
changed_when: false changed_when: false
failed_when: false failed_when: false

View File

@@ -21,7 +21,6 @@
algorithm: "{{ system_cfg.luks.pbkdf }}" algorithm: "{{ system_cfg.luks.pbkdf }}"
iteration_time: "{{ (system_cfg.luks.iter | float) / 1000 }}" iteration_time: "{{ (system_cfg.luks.iter | float) / 1000 }}"
passphrase: "{{ system_cfg.luks.passphrase | string }}" passphrase: "{{ system_cfg.luks.passphrase | string }}"
register: partitioning_luks_format_result
no_log: true no_log: true
- name: Force-close LUKS mapper - name: Force-close LUKS mapper
@@ -51,7 +50,6 @@
name: "{{ system_cfg.luks.mapper }}" name: "{{ system_cfg.luks.mapper }}"
passphrase: "{{ system_cfg.luks.passphrase | string }}" passphrase: "{{ system_cfg.luks.passphrase | string }}"
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}" allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
register: partitioning_luks_open_result
no_log: true no_log: true
rescue: rescue:
- name: Force-close stale LUKS mapper - name: Force-close stale LUKS mapper
@@ -79,7 +77,6 @@
name: "{{ system_cfg.luks.mapper }}" name: "{{ system_cfg.luks.mapper }}"
passphrase: "{{ system_cfg.luks.passphrase | string }}" passphrase: "{{ system_cfg.luks.passphrase | string }}"
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}" allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
register: partitioning_luks_open_retry
no_log: true no_log: true
- name: Get LUKS UUID - name: Get LUKS UUID

View File

@@ -56,7 +56,6 @@
- { subvol: var_log_audit } - { subvol: var_log_audit }
loop_control: loop_control:
label: "{{ item.subvol }}" label: "{{ item.subvol }}"
register: partitioning_btrfs_subvol_result
- name: Set quotas for subvolumes - name: Set quotas for subvolumes
when: system_cfg.features.cis.enabled | bool when: system_cfg.features.cis.enabled | bool
@@ -74,7 +73,6 @@
btrfs filesystem mkswapfile --size {{ partitioning_swap_size_gb }}g --uuid clear /mnt/@swap/swapfile btrfs filesystem mkswapfile --size {{ partitioning_swap_size_gb }}g --uuid clear /mnt/@swap/swapfile
args: args:
creates: /mnt/@swap/swapfile creates: /mnt/@swap/swapfile
register: partitioning_btrfs_swap_result
- name: Unmount Partition - name: Unmount Partition
ansible.posix.mount: ansible.posix.mount:

View File

@@ -0,0 +1,26 @@
---
- name: Build Xen disk definitions
when: virtualization_xen_disks is not defined
block:
- name: Compute Xen disk configuration
ansible.builtin.set_fact:
virtualization_xen_disks: "{{ virtualization_xen_disks | default([]) + [_xen_disk_cfg] }}"
vars:
device_letter_map: "{{ disk_letter_map }}"
device_letter: "{{ device_letter_map[ansible_loop.index0] }}"
_xen_disk_cfg: >-
{{
{
'path': (
virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2'
if ansible_loop.index0 == 0
else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'
),
'target': 'xvd' ~ device_letter,
'size': (item.size | float)
}
}}
loop: "{{ system_cfg.disks }}"
loop_control:
label: "{{ item | to_json }}"
extended: true

View File

@@ -70,6 +70,7 @@
- xl - xl
- destroy - destroy
- "{{ hostname }}" - "{{ hostname }}"
changed_when: false
failed_when: false failed_when: false
- name: Remove Xen VM config - name: Remove Xen VM config

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 }}"

View File

@@ -2,28 +2,7 @@
- name: Deploy VM on Xen - name: Deploy VM on Xen
block: block:
- name: Build disk definitions - name: Build disk definitions
ansible.builtin.set_fact: ansible.builtin.include_tasks: _xen_disks.yml
virtualization_xen_disks: "{{ virtualization_xen_disks | default([]) + [virtualization_xen_disk_cfg] }}"
vars:
device_letter_map: "{{ disk_letter_map }}"
device_letter: "{{ device_letter_map[ansible_loop.index0] }}"
virtualization_xen_disk_cfg: >-
{{
{
'path': (
virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2'
if ansible_loop.index0 == 0
else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'
),
'target': 'xvd' ~ device_letter,
'size': (item.size | float)
}
}}
loop: "{{ system_cfg.disks }}"
loop_control:
label: "{{ item | to_json }}"
extended: true
changed_when: false
- name: Create VM disks for Xen - name: Create VM disks for Xen
delegate_to: localhost delegate_to: localhost

View File

@@ -4,17 +4,22 @@ ssh_pwauth: true
package_update: false package_update: false
package_upgrade: false package_upgrade: false
users: users:
{% for user in system_cfg.users %} {% for username, attrs in system_cfg.users.items() %}
- name: "{{ user.name }}" - name: "{{ username }}"
primary_group: "{{ user.name }}" primary_group: "{{ username }}"
groups: users groups: users
{% if attrs.sudo | default(false) | bool %}
sudo: "ALL=(ALL) NOPASSWD:ALL" sudo: "ALL=(ALL) NOPASSWD:ALL"
passwd: "{{ user.password | password_hash('sha512') }}" {% endif %}
{% if attrs.password | default('') | length > 0 %}
passwd: "{{ attrs.password | password_hash('sha512') }}"
lock_passwd: false lock_passwd: false
{% set ssh_keys = user['keys'] | default([]) %} {% else %}
{% if ssh_keys | length > 0 %} lock_passwd: true
{% endif %}
{% if 'keys' in attrs and attrs['keys'] is iterable and attrs['keys'] is not string and attrs['keys'] | length > 0 %}
ssh_authorized_keys: ssh_authorized_keys:
{% for key in ssh_keys %} {% for key in attrs['keys'] %}
- "{{ key }}" - "{{ key }}"
{% endfor %} {% endfor %}
{% endif %} {% endif %}