Compare commits

...
30 Commits
Author SHA1 Message Date
sandwich b11d65a6f3 docs(bootstrap): document desktop, initramfs, and FDE features with SSH keepalive config 2026-04-01 15:07:58 +02:00
sandwich 3623fc292c feat(configuration): generic FDE with systemd-cryptenroll, clevis fallback, and configurable initramfs 2026-04-01 15:07:58 +02:00
sandwich dfca7ec94b fix(configuration): RedHat EFI grub wrapper with btrfs subvol prefix and boot order 2026-04-01 15:07:58 +02:00
sandwich e8be84bf49 fix(partitioning): set btrfs default subvolume and restrict @pkg to Arch 2026-04-01 15:07:58 +02:00
sandwich 322cc0b1ce fix(bootstrap): resolve interface-only network, sshd penalties, dnf scriptlets, and EFI cleanup 2026-04-01 15:07:58 +02:00
sandwich 4b38754f8b feat(bootstrap): add desktop environment support with configurable DE, DM, and display target 2026-04-01 15:07:58 +02:00
sandwich a6bc7ffe04 fix(configuration): use /boot/grub2/grub.cfg for RedHat EFI grub config 2026-03-25 16:03:39 +01:00
sandwich c529e71ebc feat(packages): add needrestart to Debian and Ubuntu package lists 2026-03-20 18:06:14 +01:00
sandwich cb46de2b6d feat(bootstrap): add full package upgrade step for Debian and Ubuntu 2026-03-20 18:05:04 +01:00
sandwich 9169117b25 fix(vim): use vimscript comment syntax for blockinfile markers in vimrc 2026-03-20 18:00:12 +01:00
sandwich 6c94c519fb fix(sudo): use explicit string check instead of bool conditional for sudo field 2026-03-20 17:31:49 +01:00
sandwich efd96a42b8 fix(connection): set ansible_port explicitly at every connection transition 2026-03-20 17:31:49 +01:00
sandwich 68661c3cca fix(vmware): use primary ansible_* vars for vmware_tools connection plugin precedence 2026-03-20 17:31:49 +01:00
sandwich 1db20c7ac0 fix(vmware): use empty password for vmware_tools during live ISO bootstrap 2026-03-20 17:31:49 +01:00
sandwich 7b155b427b fix(users): update cloud-init template and input validation for dict users 2026-03-20 17:31:49 +01:00
sandwich ca8721e98f refactor(prompts): remove vars_prompt, require users defined in inventory 2026-03-20 17:31:49 +01:00
sandwich cdb2559d8f fix(prompts): add default values to vars_prompt to skip in non-interactive mode 2026-03-20 17:31:49 +01:00
sandwich 443f6623df refactor(users): change system.users from list to dict keyed by username 2026-03-20 17:31:49 +01:00
sandwich 6cf418fe00 fix(configuration): make root password, user keys, and sudo all optional 2026-03-20 17:31:49 +01:00
sandwich 47ec5fe621 fix(cloud-init): handle missing keys and make sudo conditional 2026-03-20 17:31:49 +01:00
sandwich 240f945cce fix(cleanup): remove ansible_become override that blocks swapoff/umount 2026-03-20 17:31:49 +01:00
sandwich 663a04556f feat(global_defaults): add system.features.aur schema for validation passthrough 2026-03-20 17:31:49 +01:00
sandwich 6febd1acf1 refactor(virtualization): extract shared Xen disk definitions 2026-03-12 12:27:18 +01:00
sandwich 008187860c refactor: remove unnecessary changed_when from set_fact tasks 2026-03-12 12:25:45 +01:00
sandwich cd1be6b5e1 refactor(partitioning): remove redundant blockdev --rereadpt calls 2026-03-12 12:25:15 +01:00
sandwich 15be6149fd refactor(partitioning): remove unused register variables 2026-03-12 12:24:59 +01:00
sandwich ca29ad200d chore: suppress args[module] false positives from variable-based module_defaults 2026-03-12 12:12:27 +01:00
sandwich 8079099cee fix(cleanup): add no_log to Proxmox VM restart task 2026-03-12 12:12:27 +01:00
sandwich 9e79185b07 fix(virtualization): add missing changed_when to Xen VM stop task 2026-03-12 12:12:27 +01:00
sandwich b88bf2860f fix(configuration): replace fail+ignore_errors with debug for TPM2 fallback warning 2026-03-12 12:12:27 +01:00
45 changed files with 825 additions and 358 deletions
+1
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/
+49 -6
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`
@@ -242,7 +257,15 @@ The first user's credentials are prompted interactively via `vars_prompt` unless
| Key | Type | Default | Description | | Key | Type | Default | Description |
| -------- | ------------- | ------- | ---------------------------------------------- | | -------- | ------------- | ------- | ---------------------------------------------- |
| `device` | string | `auto` | TPM2 device selector | | `device` | string | `auto` | TPM2 device selector |
| `pcrs` | string/list | -- | PCR binding policy (e.g. `"7"` or `"0+7"`) | | `pcrs` | string/list | -- | PCR binding policy (e.g. `"7"` or `"0+7"`); empty = no PCR binding |
**TPM2 auto-unlock:** Uses `systemd-cryptenroll` on all distros. The user-set passphrase
remains as a backup unlock method. TPM2 enrollment runs in the chroot during bootstrap;
if it fails (e.g. no TPM2 hardware), the system boots with passphrase-only unlock and
TPM2 can be enrolled post-deployment via `systemd-cryptenroll --tpm2-device=auto <device>`.
On Debian/Ubuntu, TPM2 auto-unlock requires dracut (initramfs-tools does not support `tpm2-device`).
The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via `features.initramfs.generator`.
#### `system.features` #### `system.features`
@@ -259,6 +282,26 @@ The first user's credentials are prompted interactively via `vars_prompt` unless
| `banner.motd` | bool | `false` | MOTD banner | | `banner.motd` | bool | `false` | MOTD banner |
| `banner.sudo` | bool | `true` | Sudo banner | | `banner.sudo` | bool | `true` | Sudo banner |
| `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, or `systemd-nspawn` | | `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, or `systemd-nspawn` |
| `initramfs.generator` | string | auto-detected | Override initramfs generator (see below) |
| `desktop.*` | dict | see below | Desktop environment settings (see [4.2.5](#425-systemfeaturesdesktop)) |
**Initramfs generator auto-detection:** RedHat → dracut, Arch → mkinitcpio, Debian/Ubuntu → initramfs-tools.
Override with `dracut`, `mkinitcpio`, or `initramfs-tools`. When LUKS TPM2 auto-unlock is enabled and the
native generator does not support `tpm2-device`, the generator is automatically upgraded to dracut.
On distros with older dracut (no `tpm2-tss` module), clevis is used as a fallback for TPM2 binding.
#### 4.2.5 `system.features.desktop`
| Key | Type | Default | Description |
| ----------------- | ------ | -------------- | ----------------------------------------- |
| `enabled` | bool | `false` | Install desktop environment |
| `environment` | string | -- | `gnome`, `kde`, `xfce`, `sway`, `hyprland`, `cinnamon`, `mate`, `lxqt`, `budgie` |
| `display_manager` | string | auto-detected | Override DM: `gdm`, `sddm`, `lightdm`, `ly`, `greetd` |
When `enabled: true`, the bootstrap installs the desktop environment packages, enables the display manager
and bluetooth services, and sets the systemd default target to `graphical.target`.
Display manager auto-detection: gnome→gdm, kde→sddm, xfce→lightdm, sway→greetd, hyprland→ly.
### 4.3 `hypervisor` Dictionary ### 4.3 `hypervisor` Dictionary
@@ -398,7 +441,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 +451,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
+3
View File
@@ -3,3 +3,6 @@ hash_behaviour = merge
interpreter_python = auto_silent interpreter_python = auto_silent
deprecation_warnings = False deprecation_warnings = False
host_key_checking = False host_key_checking = False
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=600s -o ServerAliveInterval=30 -o ServerAliveCountMax=10
+8 -92
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
+48
View File
@@ -0,0 +1,48 @@
---
- name: Load desktop package definitions
ansible.builtin.include_vars:
file: desktop.yml
- name: Resolve desktop packages
vars:
_de: "{{ system_cfg.features.desktop.environment }}"
_family_pkgs: "{{ bootstrap_desktop_packages[os_family] | default({}) }}"
_de_config: "{{ _family_pkgs[_de] | default({}) }}"
ansible.builtin.set_fact:
_desktop_groups: "{{ _de_config.groups | default([]) }}"
_desktop_packages: "{{ _de_config.packages | default([]) }}"
- name: Validate desktop environment is supported
ansible.builtin.assert:
that:
- (_desktop_groups | length > 0) or (_desktop_packages | length > 0)
fail_msg: >-
Desktop environment '{{ system_cfg.features.desktop.environment }}'
is not defined for os_family '{{ os_family }}'.
Supported: {{ (bootstrap_desktop_packages[os_family] | default({})).keys() | join(', ') }}
quiet: true
- name: Install desktop package groups
when: _desktop_groups | length > 0
ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version }}
--setopt=install_weak_deps=False group install -y {{ _desktop_groups | join(' ') }}
register: _desktop_group_result
changed_when: _desktop_group_result.rc == 0
- name: Install desktop packages
when: _desktop_packages | length > 0
vars:
_install_commands:
RedHat: >-
{{ chroot_command }} dnf --releasever={{ os_version }}
--setopt=install_weak_deps=False install -y {{ _desktop_packages | join(' ') }}
Debian: >-
{{ chroot_command }} apt install -y {{ _desktop_packages | join(' ') }}
Archlinux: >-
pacstrap /mnt {{ _desktop_packages | join(' ') }}
Suse: >-
{{ chroot_command }} zypper install -y {{ _desktop_packages | join(' ') }}
ansible.builtin.command: "{{ _install_commands[os_family] }}"
register: _desktop_pkg_result
changed_when: _desktop_pkg_result.rc == 0
+3
View File
@@ -18,6 +18,9 @@
groupinstall -y {{ _dnf_groups }} groupinstall -y {{ _dnf_groups }}
register: bootstrap_dnf_base_result register: bootstrap_dnf_base_result
changed_when: bootstrap_dnf_base_result.rc == 0 changed_when: bootstrap_dnf_base_result.rc == 0
failed_when:
- bootstrap_dnf_base_result.rc != 0
- "'scriptlet' not in bootstrap_dnf_base_result.stderr"
- name: Ensure chroot has DNS resolution - name: Ensure chroot has DNS resolution
ansible.builtin.file: ansible.builtin.file:
+5
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 }}"
+4
View File
@@ -34,6 +34,10 @@
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}" bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}" ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"
- name: Install desktop environment packages
when: system_cfg.features.desktop.enabled | bool
ansible.builtin.include_tasks: _desktop.yml
- name: Ensure chroot uses live environment DNS - name: Ensure chroot uses live environment DNS
ansible.builtin.file: ansible.builtin.file:
src: /run/NetworkManager/resolv.conf src: /run/NetworkManager/resolv.conf
+5
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 }}"
+149
View File
@@ -0,0 +1,149 @@
---
# Per-family desktop environment package definitions.
# Keyed by os_family -> environment -> groups (dnf groupinstall) / packages.
# Kept intentionally minimal: base DE + essential tools, no full suites.
bootstrap_desktop_packages:
RedHat:
gnome:
groups:
- workstation-product-environment
packages: []
kde:
groups: []
packages:
- plasma-desktop
- plasma-nm
- plasma-pa
- plasma-systemmonitor
- sddm
- konsole
- dolphin
- kate
- kscreen
- kde-gtk-config
- xdg-user-dirs
- xdg-desktop-portal-kde
- bluez
- pipewire
- wireplumber
xfce:
groups:
- xfce-desktop-environment
packages:
- lightdm
Debian:
gnome:
groups: []
packages:
- gnome-core
- gdm3
- gnome-tweaks
- xdg-user-dirs
kde:
groups: []
packages:
- plasma-desktop
- plasma-nm
- plasma-pa
- sddm
- konsole
- dolphin
- kate
- kscreen
- xdg-user-dirs
- xdg-desktop-portal-kde
- bluez
- pipewire
- wireplumber
xfce:
groups: []
packages:
- xfce4
- xfce4-goodies
- lightdm
- xdg-user-dirs
Archlinux:
gnome:
groups: []
packages:
- gnome
- gdm
- xdg-user-dirs
kde:
groups: []
packages:
- plasma-desktop
- plasma-nm
- plasma-pa
- sddm
- konsole
- dolphin
- kate
- kscreen
- kde-gtk-config
- xdg-user-dirs
- xdg-desktop-portal-kde
- bluez
- pipewire
- wireplumber
xfce:
groups: []
packages:
- xfce4
- xfce4-goodies
- lightdm
- xdg-user-dirs
sway:
groups: []
packages:
- sway
- waybar
- foot
- wofi
- greetd
- xdg-user-dirs
- xdg-desktop-portal-wlr
- bluez
- pipewire
- wireplumber
hyprland:
groups: []
packages:
- hyprland
- kitty
- wofi
- waybar
- ly
- xdg-user-dirs
- xdg-desktop-portal-hyprland
- polkit-kde-agent
- qt5-wayland
- qt6-wayland
- bluez
- pipewire
- wireplumber
Suse:
gnome:
groups: []
packages:
- patterns-gnome-gnome_basic
- gdm
- xdg-user-dirs
kde:
groups: []
packages:
- patterns-kde-kde_plasma
- sddm
- xdg-user-dirs
# Display manager auto-detection from desktop environment.
bootstrap_desktop_dm_map:
gnome: gdm
kde: sddm
xfce: lightdm
sway: greetd
hyprland: ly@tty2
cinnamon: lightdm
mate: lightdm
lxqt: sddm
budgie: gdm
+2
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
+6 -6
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,12 @@
| regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "") | regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "")
| trim | trim
}} }}
changed_when: false
- name: Ensure boot device is set to hard disk in VM XML
when: "'<boot ' not in cleanup_libvirt_domain_xml_clean"
ansible.builtin.set_fact:
cleanup_libvirt_domain_xml_clean: >-
{{ cleanup_libvirt_domain_xml_clean | regex_replace('(</type>)', '\1\n <boot dev="hd"/>') }}
- name: Update VM definition without installer media - name: Update VM definition without installer media
community.libvirt.virt: community.libvirt.virt:
+1
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
+1 -24
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
+50 -8
View File
@@ -34,6 +34,16 @@
register: configuration_efi_entry_result register: configuration_efi_entry_result
changed_when: configuration_efi_entry_result.rc == 0 changed_when: configuration_efi_entry_result.rc == 0
- name: Set installed OS as first EFI boot entry
ansible.builtin.shell:
cmd: >-
set -o pipefail &&
efibootmgr | grep -i '{{ _efi_vendor }}' | grep -oP 'Boot\K[0-9A-F]+' | head -1
| xargs -I{} efibootmgr -o {}
executable: /bin/bash
register: _efi_bootorder_result
changed_when: _efi_bootorder_result.rc == 0
- name: Ensure lvm2 for non btrfs filesystems - name: Ensure lvm2 for non btrfs filesystems
when: os == "archlinux" and system_cfg.filesystem != "btrfs" when: os == "archlinux" and system_cfg.filesystem != "btrfs"
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
@@ -48,16 +58,48 @@
register: configuration_initramfs_result register: configuration_initramfs_result
changed_when: configuration_initramfs_result.rc == 0 changed_when: configuration_initramfs_result.rc == 0
- name: Generate grub config - name: Generate grub config (RedHat)
when: os_family == 'RedHat'
ansible.builtin.command: >-
{{ chroot_command }} /usr/sbin/{{ _configuration_platform.grub_mkconfig_prefix }}
-o /boot/grub2/grub.cfg
register: configuration_grub_result
changed_when: configuration_grub_result.rc == 0
- name: Fix btrfs BLS boot variable in grub config
when:
- os_family == 'RedHat'
- system_cfg.filesystem == 'btrfs'
ansible.builtin.replace:
path: /mnt/boot/grub2/grub.cfg
regexp: 'search --no-floppy --fs-uuid --set=boot \S+'
replace: 'set boot=$root'
- name: Create EFI grub.cfg wrapper for RedHat
when: os_family == 'RedHat'
vars: vars:
configuration_grub_cfg_cmd: >- _grub2_path: >-
{{ {{
'/usr/sbin/' + _configuration_platform.grub_mkconfig_prefix + ' -o ' '/grub2'
+ partitioning_efi_mountpoint if (partitioning_separate_boot | bool)
+ '/EFI/' + _efi_vendor + '/grub.cfg' else ('/@/boot/grub2' if system_cfg.filesystem == 'btrfs' else '/boot/grub2')
if os_family == 'RedHat'
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
}} }}
ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}" ansible.builtin.shell:
cmd: |
set -o pipefail
uuid=$(grep -m1 'search.*--set=root' /mnt/boot/grub2/grub.cfg | grep -oP '[\da-f]{8}(-[\da-f]{4}){3}-[\da-f]{12}')
cat > /mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/grub.cfg <<GRUBEOF
search --no-floppy --fs-uuid --set=dev $uuid
set prefix=(\$dev){{ _grub2_path }}
export \$prefix
configfile \$prefix/grub.cfg
GRUBEOF
executable: /bin/bash
register: _grub_wrapper_result
changed_when: _grub_wrapper_result.rc == 0
- name: Generate grub config (non-RedHat)
when: os_family != 'RedHat'
ansible.builtin.command: "{{ chroot_command }} /usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg"
register: configuration_grub_result register: configuration_grub_result
changed_when: configuration_grub_result.rc == 0 changed_when: configuration_grub_result.rc == 0
+20 -9
View File
@@ -36,6 +36,12 @@
configuration_luks_tpm2_device: "{{ system_cfg.luks.tpm2.device }}" configuration_luks_tpm2_device: "{{ system_cfg.luks.tpm2.device }}"
configuration_luks_tpm2_pcrs: "{{ luks_tpm2_pcrs }}" configuration_luks_tpm2_pcrs: "{{ luks_tpm2_pcrs }}"
configuration_luks_keyfile_path: "/etc/cryptsetup-keys.d/{{ system_cfg.luks.mapper }}.key" configuration_luks_keyfile_path: "/etc/cryptsetup-keys.d/{{ system_cfg.luks.mapper }}.key"
configuration_luks_tpm2_token_lib: >-
{{
'/usr/lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-systemd-tpm2.so'
if os_family == 'Debian'
else '/usr/lib64/cryptsetup/libcryptsetup-token-systemd-tpm2.so'
}}
- name: Validate LUKS UUID is available - name: Validate LUKS UUID is available
ansible.builtin.assert: ansible.builtin.assert:
@@ -51,8 +57,13 @@
fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt. fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt.
no_log: true no_log: true
- name: Enroll TPM2 for LUKS - name: Detect TPM2 unlock method
when: configuration_luks_auto_method == 'tpm2' ansible.builtin.include_tasks: encryption/initramfs_detect.yml
- name: Enroll TPM2 via systemd-cryptenroll
when:
- configuration_luks_auto_method == 'tpm2'
- _tpm2_method | default('systemd-cryptenroll') == 'systemd-cryptenroll'
ansible.builtin.include_tasks: encryption/tpm2.yml ansible.builtin.include_tasks: encryption/tpm2.yml
- name: Configure LUKS keyfile auto-decrypt - name: Configure LUKS keyfile auto-decrypt
@@ -78,7 +89,7 @@
}} }}
luks_tpm2_option_list: >- luks_tpm2_option_list: >-
{{ {{
(configuration_luks_auto_method == 'tpm2') (configuration_luks_auto_method == 'tpm2' and (_tpm2_method | default('systemd-cryptenroll')) == 'systemd-cryptenroll')
| ternary( | ternary(
['tpm2-device=' + configuration_luks_tpm2_device] ['tpm2-device=' + configuration_luks_tpm2_device]
+ (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs] + (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
@@ -122,16 +133,16 @@
path: /mnt{{ configuration_luks_keyfile_path }} path: /mnt{{ configuration_luks_keyfile_path }}
state: absent state: absent
- name: Configure initramfs for LUKS
ansible.builtin.include_tasks: encryption/initramfs.yml
- name: Configure crypttab - name: Configure crypttab
ansible.builtin.include_tasks: encryption/crypttab.yml ansible.builtin.include_tasks: encryption/crypttab.yml
- name: Configure initramfs - name: Configure dracut for LUKS
ansible.builtin.include_tasks: encryption/initramfs.yml when: _initramfs_generator | default('') == 'dracut'
- name: Configure dracut
when: os_family == 'RedHat'
ansible.builtin.include_tasks: encryption/dracut.yml ansible.builtin.include_tasks: encryption/dracut.yml
- name: Configure GRUB for LUKS - name: Configure GRUB for LUKS
when: not os_family == 'RedHat' when: _initramfs_generator | default('') != 'dracut' or os_family != 'RedHat'
ansible.builtin.include_tasks: encryption/grub.yml ansible.builtin.include_tasks: encryption/grub.yml
+28 -18
View File
@@ -9,48 +9,58 @@
ansible.builtin.copy: ansible.builtin.copy:
dest: /mnt/etc/dracut.conf.d/crypt.conf dest: /mnt/etc/dracut.conf.d/crypt.conf
content: | content: |
add_dracutmodules+=" crypt " add_dracutmodules+=" crypt systemd "
{% if configuration_luks_keyfile_in_use %} {% if configuration_luks_keyfile_in_use | default(false) %}
install_items+=" {{ configuration_luks_keyfile_path }} " install_items+=" {{ configuration_luks_keyfile_path }} "
{% endif %} {% endif %}
{% if configuration_luks_auto_method == 'tpm2' %}
install_items+=" {{ configuration_luks_tpm2_token_lib | default('') }} "
{% endif %}
mode: "0644" mode: "0644"
- name: Read kernel cmdline defaults # --- Kernel cmdline: write rd.luks.* args for dracut ---
- name: Ensure kernel cmdline directory exists
ansible.builtin.file:
path: /mnt/etc/kernel
state: directory
mode: "0755"
- name: Read existing kernel cmdline
ansible.builtin.slurp: ansible.builtin.slurp:
src: /mnt/etc/kernel/cmdline src: /mnt/etc/kernel/cmdline
register: configuration_kernel_cmdline_slurp register: _kernel_cmdline_slurp
failed_when: false
- name: Build kernel cmdline with LUKS args - name: Build kernel cmdline with LUKS args
vars: vars:
kernel_cmdline_current: >- _cmdline_current: >-
{{ configuration_kernel_cmdline_slurp.content | b64decode | trim }} {{ (_kernel_cmdline_slurp.content | default('') | b64decode | default('')) | trim }}
kernel_cmdline_list: >- _cmdline_list: >-
{{ _cmdline_current.split() if _cmdline_current | length > 0 else [] }}
_cmdline_filtered: >-
{{ {{
kernel_cmdline_current.split() _cmdline_list
if kernel_cmdline_current | length > 0 else []
}}
kernel_cmdline_filtered: >-
{{
kernel_cmdline_list
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=') | reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
| list | list
}} }}
kernel_cmdline_new: >- _cmdline_new: >-
{{ {{
(kernel_cmdline_filtered + configuration_luks_kernel_args.split()) (_cmdline_filtered + configuration_luks_kernel_args.split())
| unique | unique
| join(' ') | join(' ')
}} }}
ansible.builtin.set_fact: ansible.builtin.set_fact:
configuration_kernel_cmdline_new: "{{ kernel_cmdline_new }}" _dracut_kernel_cmdline: "{{ _cmdline_new }}"
- name: Write kernel cmdline with LUKS args - name: Write kernel cmdline with LUKS args
ansible.builtin.copy: ansible.builtin.copy:
dest: /mnt/etc/kernel/cmdline dest: /mnt/etc/kernel/cmdline
mode: "0644" mode: "0644"
content: "{{ configuration_kernel_cmdline_new }}\n" content: "{{ _dracut_kernel_cmdline }}\n"
# --- BLS entries: RedHat-specific ---
- name: Update BLS entries with LUKS kernel cmdline - name: Update BLS entries with LUKS kernel cmdline
when: os_family == 'RedHat'
vars: vars:
_bls_cmdline: "{{ configuration_kernel_cmdline_new }}" _bls_cmdline: "{{ _dracut_kernel_cmdline }}"
ansible.builtin.include_tasks: ../_bls_update.yml ansible.builtin.include_tasks: ../_bls_update.yml
@@ -1,8 +1,94 @@
--- ---
- name: Ensure keyfile pattern for initramfs-tools # Initramfs configuration for LUKS auto-unlock.
# Runs AFTER Build LUKS parameters (so configuration_luks_keyfile_in_use is set).
# _initramfs_generator and _tpm2_method are set by initramfs_detect.yml.
# --- clevis: install and bind TPM2 ---
- name: Install clevis in target system
when: when:
- os_family == 'Debian' - configuration_luks_auto_method == 'tpm2'
- configuration_luks_keyfile_in_use - _tpm2_method | default('') == 'clevis'
ansible.builtin.command: >-
{{ chroot_command }} apt install -y clevis clevis-luks clevis-tpm2 clevis-initramfs tpm2-tools
register: _clevis_install_result
changed_when: _clevis_install_result.rc == 0
- name: Install clevis on installer for LUKS binding
when:
- configuration_luks_auto_method == 'tpm2'
- _tpm2_method | default('') == 'clevis'
community.general.pacman:
name:
- clevis
- tpm2-tools
state: present
retries: 3
delay: 5
- name: Create clevis passphrase file
when:
- configuration_luks_auto_method == 'tpm2'
- _tpm2_method | default('') == 'clevis'
ansible.builtin.copy:
dest: /mnt/root/.luks-enroll-key
content: "{{ configuration_luks_passphrase }}"
mode: "0600"
no_log: true
- name: Ensure TPM device accessible for clevis
when:
- configuration_luks_auto_method == 'tpm2'
- _tpm2_method | default('') == 'clevis'
ansible.builtin.shell: >-
ls /mnt/dev/tpmrm0 2>/dev/null
|| (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0)
changed_when: false
failed_when: false
- name: Bind LUKS to TPM2 via clevis
when:
- configuration_luks_auto_method == 'tpm2'
- _tpm2_method | default('') == 'clevis'
vars:
_clevis_config: >-
{{
'{"pcr_ids":"' + configuration_luks_tpm2_pcrs + '"}'
if configuration_luks_tpm2_pcrs | length > 0
else '{}'
}}
ansible.builtin.command: >-
clevis luks bind -f -k /mnt/root/.luks-enroll-key
-d {{ configuration_luks_device }} tpm2 '{{ _clevis_config }}'
register: _clevis_bind_result
changed_when: _clevis_bind_result.rc == 0
failed_when: false
# Initramfs regeneration is handled by the bootloader task which runs after
# encryption configuration. Clevis hooks are included automatically by
# update-initramfs when clevis-initramfs is installed.
- name: Remove clevis passphrase file
when:
- configuration_luks_auto_method == 'tpm2'
- _tpm2_method | default('') == 'clevis'
ansible.builtin.file:
path: /mnt/root/.luks-enroll-key
state: absent
- name: Report clevis binding result
when:
- configuration_luks_auto_method == 'tpm2'
- _tpm2_method | default('') == 'clevis'
ansible.builtin.debug:
msg: >-
{{ 'Clevis TPM2 binding succeeded' if (_clevis_bind_result.rc | default(1)) == 0
else 'Clevis TPM2 binding failed: ' + (_clevis_bind_result.stderr | default('unknown')) + '. System will require passphrase at boot.' }}
# --- initramfs-tools: keyfile support (non-TPM2) ---
- name: Configure initramfs-tools keyfile pattern
when:
- _initramfs_generator | default('') == 'initramfs-tools'
- configuration_luks_keyfile_in_use | default(false) | bool
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: /mnt/etc/cryptsetup-initramfs/conf-hook path: /mnt/etc/cryptsetup-initramfs/conf-hook
regexp: "^KEYFILE_PATTERN=" regexp: "^KEYFILE_PATTERN="
@@ -10,8 +96,9 @@
create: true create: true
mode: "0644" mode: "0644"
# --- mkinitcpio: systemd + sd-encrypt hooks ---
- name: Configure mkinitcpio hooks for LUKS - name: Configure mkinitcpio hooks for LUKS
when: os == 'archlinux' when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: /mnt/etc/mkinitcpio.conf path: /mnt/etc/mkinitcpio.conf
regexp: "^HOOKS=" regexp: "^HOOKS="
@@ -20,13 +107,13 @@
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck) block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
- name: Read mkinitcpio configuration - name: Read mkinitcpio configuration
when: os == 'archlinux' when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.slurp: ansible.builtin.slurp:
src: /mnt/etc/mkinitcpio.conf src: /mnt/etc/mkinitcpio.conf
register: configuration_mkinitcpio_slurp register: configuration_mkinitcpio_slurp
- name: Build mkinitcpio FILES list - name: Build mkinitcpio FILES list
when: os == 'archlinux' when: _initramfs_generator | default('') == 'mkinitcpio'
vars: vars:
mkinitcpio_files_list: >- mkinitcpio_files_list: >-
{{ {{
@@ -42,7 +129,7 @@
{{ {{
( (
(mkinitcpio_files_list + [configuration_luks_keyfile_path]) (mkinitcpio_files_list + [configuration_luks_keyfile_path])
if configuration_luks_keyfile_in_use if (configuration_luks_keyfile_in_use | default(false))
else ( else (
mkinitcpio_files_list mkinitcpio_files_list
| reject('equalto', configuration_luks_keyfile_path) | reject('equalto', configuration_luks_keyfile_path)
@@ -55,7 +142,7 @@
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}" configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
- name: Configure mkinitcpio FILES list - name: Configure mkinitcpio FILES list
when: os == 'archlinux' when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: /mnt/etc/mkinitcpio.conf path: /mnt/etc/mkinitcpio.conf
regexp: "^FILES=" regexp: "^FILES="
@@ -0,0 +1,98 @@
---
# Resolve initramfs generator and TPM2 unlock method.
# Sets _initramfs_generator and _tpm2_method facts.
#
# Generator detection: derived from the platform's initramfs_cmd
# (dracut → dracut, mkinitcpio → mkinitcpio, else → initramfs-tools)
# TPM2 method: systemd-cryptenroll when generator supports tpm2-device,
# clevis fallback otherwise. Non-native dracut installed automatically.
- name: Resolve initramfs generator
vars:
_user_generator: "{{ system_cfg.features.initramfs.generator | default('') }}"
_native_generator: >-
{{
'dracut' if _configuration_platform.initramfs_cmd is search('dracut')
else ('mkinitcpio' if _configuration_platform.initramfs_cmd is search('mkinitcpio')
else 'initramfs-tools')
}}
ansible.builtin.set_fact:
_initramfs_generator: >-
{{ _user_generator if _user_generator | length > 0 else _native_generator }}
_initramfs_native_generator: "{{ _native_generator }}"
# --- Install non-native dracut if overridden or needed ---
- name: Install dracut in chroot when not native
when:
- _initramfs_generator == 'dracut'
- _initramfs_native_generator != 'dracut'
ansible.builtin.shell: >-
{{ chroot_command }} sh -c '
command -v apt >/dev/null 2>&1 && apt install -y dracut ||
command -v pacman >/dev/null 2>&1 && pacman -S --noconfirm dracut ||
command -v dnf >/dev/null 2>&1 && dnf install -y dracut
'
register: _dracut_install_result
changed_when: _dracut_install_result.rc == 0
failed_when: false
- name: Override initramfs command to dracut
when:
- _initramfs_generator == 'dracut'
- _initramfs_native_generator != 'dracut'
vars:
# Generate dracut initramfs with output name matching what GRUB expects:
# mkinitcpio native: /boot/initramfs-linux.img (Arch convention)
# initramfs-tools native: /boot/initrd.img-<kver> (Debian convention)
_dracut_cmd: >-
{{
'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initramfs-linux.img $kver; done"'
if _initramfs_native_generator == 'mkinitcpio'
else 'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"'
}}
ansible.builtin.set_fact:
_configuration_platform: >-
{{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }}
# --- TPM2 method detection ---
- name: Probe dracut for TPM2 module support
when:
- configuration_luks_auto_method == 'tpm2'
- _initramfs_generator != 'mkinitcpio'
ansible.builtin.command: "{{ chroot_command }} dracut --list-modules"
register: _dracut_modules_check
changed_when: false
failed_when: false
- name: Resolve TPM2 unlock method
when: configuration_luks_auto_method == 'tpm2'
vars:
# mkinitcpio sd-encrypt supports tpm2-device natively
# dracut with tpm2-tss module supports tpm2-device natively
# everything else needs clevis
_supports_tpm2_native: >-
{{
_initramfs_generator == 'mkinitcpio'
or ('tpm2-tss' in (_dracut_modules_check.stdout | default('')))
}}
ansible.builtin.set_fact:
_tpm2_method: "{{ 'systemd-cryptenroll' if _supports_tpm2_native | bool else 'clevis' }}"
# --- Auto-upgrade to dracut when tpm2-tss available but generator isn't dracut ---
- name: Switch to dracut for TPM2 support
when:
- configuration_luks_auto_method == 'tpm2'
- _tpm2_method == 'systemd-cryptenroll'
- _initramfs_generator not in ['dracut', 'mkinitcpio']
vars:
_dracut_cmd: >-
bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"
ansible.builtin.set_fact:
_initramfs_generator: dracut
_configuration_platform: >-
{{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }}
- name: Report TPM2 configuration
when: configuration_luks_auto_method == 'tpm2'
ansible.builtin.debug:
msg: "TPM2 unlock: {{ _tpm2_method | default('none') }} | initramfs: {{ _initramfs_generator }}"
@@ -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
+28 -61
View File
@@ -1,26 +1,35 @@
--- ---
# TPM2 enrollment via systemd-cryptenroll.
# Works with dracut and mkinitcpio (sd-encrypt). The user-set passphrase
# remains as a backup unlock method — no auto-generated keyfiles.
- name: Enroll TPM2 for LUKS - name: Enroll TPM2 for LUKS
block: block:
# Tempfile in chroot /tmp — accessible by both chroot and host commands
- name: Create temporary passphrase file for TPM2 enrollment - name: Create temporary passphrase file for TPM2 enrollment
ansible.builtin.tempfile: ansible.builtin.tempfile:
path: /mnt/tmp path: /mnt/root
prefix: luks-passphrase- prefix: luks-passphrase-
state: file state: file
register: configuration_luks_tpm2_passphrase_tempfile register: _tpm2_passphrase_tempfile
- name: Write passphrase into temporary file for TPM2 enrollment - name: Write passphrase into temporary file
ansible.builtin.copy: ansible.builtin.copy:
dest: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}" dest: "{{ _tpm2_passphrase_tempfile.path }}"
content: "{{ configuration_luks_passphrase }}" content: "{{ configuration_luks_passphrase }}"
owner: root owner: root
group: root group: root
mode: "0600" mode: "0600"
no_log: true no_log: true
- name: Enroll TPM2 token - name: Ensure TPM device is accessible in chroot
ansible.builtin.shell: >-
ls /mnt/dev/tpmrm0 2>/dev/null
|| (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0)
changed_when: false
failed_when: false
- name: Enroll TPM2 token via systemd-cryptenroll
vars: vars:
configuration_luks_enroll_args: >- _enroll_args: >-
{{ {{
[ [
'/usr/bin/systemd-cryptenroll', '/usr/bin/systemd-cryptenroll',
@@ -28,70 +37,28 @@
'--tpm2-with-pin=false', '--tpm2-with-pin=false',
'--wipe-slot=tpm2', '--wipe-slot=tpm2',
'--unlock-key-file=' + ( '--unlock-key-file=' + (
configuration_luks_tpm2_passphrase_tempfile.path _tpm2_passphrase_tempfile.path | regex_replace('^/mnt', '')
| regex_replace('^/mnt', '')
) )
] ]
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs] + (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
if configuration_luks_tpm2_pcrs | length > 0 else []) if configuration_luks_tpm2_pcrs | length > 0 else [])
+ [configuration_luks_device] + [configuration_luks_device]
}} }}
configuration_luks_enroll_chroot_cmd: >- ansible.builtin.command: "{{ chroot_command }} {{ _enroll_args | join(' ') }}"
{{ chroot_command }} {{ configuration_luks_enroll_args | join(' ') }} register: _tpm2_enroll_result
ansible.builtin.command: "{{ configuration_luks_enroll_chroot_cmd }}" changed_when: _tpm2_enroll_result.rc == 0
register: configuration_luks_tpm2_enroll_chroot
changed_when: configuration_luks_tpm2_enroll_chroot.rc == 0
failed_when: false
- name: Retry TPM2 enrollment in installer environment
when:
- (configuration_luks_tpm2_enroll_chroot.rc | default(1)) != 0
vars:
configuration_luks_enroll_args: >-
{{
[
'/usr/bin/systemd-cryptenroll',
'--tpm2-device=' + configuration_luks_tpm2_device,
'--tpm2-with-pin=false',
'--wipe-slot=tpm2',
'--unlock-key-file=' + configuration_luks_tpm2_passphrase_tempfile.path
]
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
if configuration_luks_tpm2_pcrs | length > 0 else [])
+ [configuration_luks_device]
}}
ansible.builtin.command:
argv: "{{ configuration_luks_enroll_args }}"
register: configuration_luks_tpm2_enroll_host
changed_when: configuration_luks_tpm2_enroll_host.rc == 0
failed_when: false
- name: Validate TPM2 enrollment succeeded
ansible.builtin.assert:
that:
- >-
(configuration_luks_tpm2_enroll_chroot.rc | default(1)) == 0
or (configuration_luks_tpm2_enroll_host.rc | default(1)) == 0
fail_msg: >-
TPM2 enrollment failed.
chroot rc={{ configuration_luks_tpm2_enroll_chroot.rc | default('n/a') }},
host rc={{ configuration_luks_tpm2_enroll_host.rc | default('n/a') }},
chroot stderr={{ configuration_luks_tpm2_enroll_chroot.stderr | default('') }},
host stderr={{ configuration_luks_tpm2_enroll_host.stderr | default('') }}
rescue: rescue:
- name: Warn about TPM2 enrollment failure - name: TPM2 enrollment failed
ansible.builtin.fail: ansible.builtin.debug:
msg: >- msg: >-
WARNING: TPM2 enrollment failed — falling back to keyfile auto-decrypt. TPM2 enrollment failed: {{ _tpm2_enroll_result.stderr | default('unknown') }}.
The system will use a keyfile instead of TPM2 for automatic LUKS unlock. The system will require the passphrase for LUKS unlock on boot.
ignore_errors: true TPM2 can be enrolled post-deployment via: systemd-cryptenroll --tpm2-device=auto {{ configuration_luks_device }}
- name: Fallback to keyfile auto-decrypt
ansible.builtin.set_fact:
configuration_luks_auto_method: keyfile
always: always:
- name: Remove TPM2 enrollment passphrase file - name: Remove temporary passphrase file
when: configuration_luks_tpm2_passphrase_tempfile.path is defined when: _tpm2_passphrase_tempfile.path is defined
ansible.builtin.file: ansible.builtin.file:
path: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}" path: "{{ _tpm2_passphrase_tempfile.path }}"
state: absent state: absent
+1 -1
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
+25
View File
@@ -2,6 +2,12 @@
- name: Enable systemd services - name: Enable systemd services
when: _configuration_platform.init_system == 'systemd' when: _configuration_platform.init_system == 'systemd'
vars: vars:
_desktop_dm: >-
{{
system_cfg.features.desktop.display_manager
if (system_cfg.features.desktop.display_manager | length > 0)
else (configuration_desktop_dm_map[system_cfg.features.desktop.environment] | default(''))
}}
configuration_systemd_services: >- configuration_systemd_services: >-
{{ {{
['NetworkManager'] ['NetworkManager']
@@ -9,12 +15,31 @@
+ (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else []) + (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else [])
+ ([_configuration_platform.ssh_service] if system_cfg.features.ssh.enabled | bool else []) + ([_configuration_platform.ssh_service] if system_cfg.features.ssh.enabled | bool else [])
+ (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' else []) + (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' else [])
+ ([_desktop_dm] if system_cfg.features.desktop.enabled | bool and _desktop_dm | length > 0 else [])
+ (['bluetooth'] if system_cfg.features.desktop.enabled | bool else [])
}} }}
ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ item }}" ansible.builtin.command: "{{ chroot_command }} systemctl enable {{ item }}"
loop: "{{ configuration_systemd_services }}" loop: "{{ configuration_systemd_services }}"
register: configuration_enable_service_result register: configuration_enable_service_result
changed_when: configuration_enable_service_result.rc == 0 changed_when: configuration_enable_service_result.rc == 0
- name: Activate UFW firewall
when:
- system_cfg.features.firewall.backend == 'ufw'
- system_cfg.features.firewall.enabled | bool
ansible.builtin.command: "{{ chroot_command }} ufw --force enable"
register: _ufw_enable_result
changed_when: _ufw_enable_result.rc == 0
failed_when: false
- name: Set default systemd target to graphical
when:
- _configuration_platform.init_system == 'systemd'
- system_cfg.features.desktop.enabled | bool
ansible.builtin.command: "{{ chroot_command }} systemctl set-default graphical.target"
register: _desktop_target_result
changed_when: _desktop_target_result.rc == 0
- name: Enable OpenRC services - name: Enable OpenRC services
when: _configuration_platform.init_system == 'openrc' when: _configuration_platform.init_system == 'openrc'
vars: vars:
+6 -6
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 }}"
+32 -25
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 }}"
+12
View File
@@ -65,3 +65,15 @@ configuration_platform_config:
grub_mkconfig_prefix: grub-mkconfig grub_mkconfig_prefix: grub-mkconfig
locale_gen: false locale_gen: false
init_system: runit init_system: runit
# Display manager auto-detection from desktop environment name.
configuration_desktop_dm_map:
gnome: gdm
kde: sddm
xfce: lightdm
sway: greetd
hyprland: ly@tty2
cinnamon: lightdm
mate: lightdm
lxqt: sddm
budgie: gdm
@@ -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
+17
View File
@@ -68,6 +68,23 @@
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry. Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
quiet: true quiet: true
- name: Harden sshd for Ansible automation
ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config
marker: "# {mark} BOOTSTRAP ANSIBLE SETTINGS"
block: |
PerSourcePenalties no
MaxStartups 50:30:100
ClientAliveInterval 30
ClientAliveCountMax 10
register: _sshd_config_result
- name: Restart sshd immediately if config was changed
when: _sshd_config_result is changed
ansible.builtin.service:
name: sshd
state: restarted
- name: Abort if the host is not booted from the Arch install media - name: Abort if the host is not booted from the Arch install media
when: when:
- not (custom_iso | bool) - not (custom_iso | bool)
@@ -25,6 +25,7 @@
state: latest state: latest
loop: loop:
- { name: glibc } - { name: glibc }
- { name: lua, os: [almalinux, fedora, rhel, rocky] }
- { name: dnf, os: [almalinux, fedora, rhel, rocky] } - { name: dnf, os: [almalinux, fedora, rhel, rocky] }
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] } - { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
- { name: debian-archive-keyring, os: [debian] } - { name: debian-archive-keyring, os: [debian] }
+11 -1
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,8 +129,18 @@ 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
initramfs:
generator: "" # auto-detected; override: dracut|mkinitcpio|initramfs-tools
desktop:
enabled: false
environment: "" # gnome|kde|xfce|sway|hyprland|cinnamon|mate|lxqt|budgie
display_manager: "" # auto from environment when empty; override: gdm|sddm|lightdm|greetd
# Per-hypervisor required fields — drives data-driven validation. # Per-hypervisor required fields — drives data-driven validation.
# All virtual types additionally require network bridge or interfaces. # All virtual types additionally require network bridge or interfaces.
@@ -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 }}"
@@ -144,27 +144,13 @@
url: "{{ system_raw.features.rhel_repo.url | default('') | string }}" url: "{{ system_raw.features.rhel_repo.url | default('') | string }}"
chroot: chroot:
tool: "{{ system_raw.features.chroot.tool | string }}" tool: "{{ system_raw.features.chroot.tool | string }}"
initramfs:
generator: "{{ system_raw.features.initramfs.generator | default('') | string | lower }}"
desktop:
enabled: "{{ system_raw.features.desktop.enabled | bool }}"
environment: "{{ system_raw.features.desktop.environment | default('') | string | lower }}"
display_manager: "{{ system_raw.features.desktop.display_manager | default('') | string | lower }}"
hostname: "{{ system_name }}" hostname: "{{ system_name }}"
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}" os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
os_version: "{{ system_raw.version | default('') | string }}" os_version: "{{ system_raw.version | default('') | string }}"
no_log: true no_log: true
- name: Populate primary network fields from first interface
when:
- system_cfg.network.interfaces | length > 0
- system_cfg.network.bridge | default('') | string | length == 0
vars:
_primary: "{{ system_cfg.network.interfaces[0] }}"
ansible.builtin.set_fact:
system_cfg: >-
{{
system_cfg | combine({
'network': system_cfg.network | combine({
'bridge': _primary.bridge | default(''),
'vlan': _primary.vlan | default(''),
'ip': _primary.ip | default(''),
'prefix': _primary.prefix | default(''),
'gateway': _primary.gateway | default('')
})
}, recursive=True)
}}
@@ -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
+11 -7
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
+21
View File
@@ -17,6 +17,27 @@
- name: Normalize disk configuration - name: Normalize disk configuration
ansible.builtin.include_tasks: _normalize_disks.yml ansible.builtin.include_tasks: _normalize_disks.yml
- name: Populate primary network fields from first interface
when:
- system_cfg is defined
- system_cfg.network.interfaces | default([]) | length > 0
- system_cfg.network.ip | default('') | string | length == 0
vars:
_primary: "{{ system_cfg.network.interfaces[0] }}"
ansible.builtin.set_fact:
system_cfg: >-
{{
system_cfg | combine({
'network': system_cfg.network | combine({
'bridge': _primary.bridge | default(''),
'vlan': _primary.vlan | default(''),
'ip': _primary.ip | default(''),
'prefix': _primary.prefix | default(''),
'gateway': _primary.gateway | default('')
})
}, recursive=True)
}}
- name: Check if pre-computed system_cfg needs enrichment - name: Check if pre-computed system_cfg needs enrichment
when: system_cfg is defined when: system_cfg is defined
ansible.builtin.set_fact: ansible.builtin.set_fact:
+8 -5
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
@@ -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
@@ -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
+3 -2
View File
@@ -9,12 +9,13 @@
- >- - >-
system_cfg.features.cis.enabled | bool or ( system_cfg.features.cis.enabled | bool or (
not (system_cfg.features.cis.enabled | bool) and ( not (system_cfg.features.cis.enabled | bool) and (
(system_cfg.filesystem == 'btrfs' and item.path in ['/home', '/var/log', '/var/cache/pacman/pkg']) (system_cfg.filesystem == 'btrfs' and item.path in ['/home', '/var/log']
+ (['/var/cache/pacman/pkg'] if os == 'archlinux' else []))
or (item.path not in ['/home', '/var', '/var/log', '/var/log/audit', '/var/cache/pacman/pkg']) or (item.path not in ['/home', '/var', '/var/log', '/var/log/audit', '/var/cache/pacman/pkg'])
) )
) )
- >- - >-
not (item.path in ['/swap', '/var/cache/pacman/pkg'] and system_cfg.filesystem != 'btrfs') not (item.path in ['/swap', '/var/cache/pacman/pkg'] and (system_cfg.filesystem != 'btrfs' or os != 'archlinux'))
- system_cfg.features.swap.enabled | bool or item.path != '/swap' - system_cfg.features.swap.enabled | bool or item.path != '/swap'
ansible.posix.mount: ansible.posix.mount:
path: /mnt{{ item.path }} path: /mnt{{ item.path }}
-3
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
+9 -3
View File
@@ -43,6 +43,7 @@
when: when:
- system_cfg.features.cis.enabled | bool or item.subvol not in ['var_log_audit'] - system_cfg.features.cis.enabled | bool or item.subvol not in ['var_log_audit']
- system_cfg.features.swap.enabled | bool or item.subvol != 'swap' - system_cfg.features.swap.enabled | bool or item.subvol != 'swap'
- item.os is not defined or os in item.os
ansible.builtin.command: btrfs su cr /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }} ansible.builtin.command: btrfs su cr /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
args: args:
creates: /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }} creates: /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
@@ -51,12 +52,18 @@
- { subvol: swap } - { subvol: swap }
- { subvol: home } - { subvol: home }
- { subvol: var } - { subvol: var }
- { subvol: pkg } - { subvol: pkg, os: [archlinux] }
- { subvol: var_log } - { subvol: var_log }
- { 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 default btrfs subvolume to @
ansible.builtin.shell: >-
btrfs subvolume list /mnt | awk '/ path @$/ {print $2}'
| xargs -I{} btrfs subvolume set-default {} /mnt
register: partitioning_btrfs_default_result
changed_when: partitioning_btrfs_default_result.rc == 0
- 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 +81,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:
+26
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
+5 -4
View File
@@ -40,10 +40,10 @@
failed_when: false failed_when: false
- name: Undefine libvirt VM - name: Undefine libvirt VM
community.libvirt.virt: ansible.builtin.command:
name: "{{ hostname }}" cmd: "virsh -c {{ libvirt_uri | default('qemu:///system') }} undefine {{ hostname }} --nvram"
command: undefine register: _libvirt_undefine_result
uri: "{{ libvirt_uri | default('qemu:///system') }}" changed_when: _libvirt_undefine_result.rc == 0
failed_when: false failed_when: false
- name: Remove libvirt disk images - name: Remove libvirt disk images
@@ -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
+2 -2
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 }}"
+1 -22
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
@@ -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 %}