Compare commits

...

38 Commits

Author SHA1 Message Date
6bfaa0aa2b fix(configuration): guard user keys access to avoid dict.keys() method collision 2026-04-16 15:37:13 +02:00
5dc0424acb feat(cleanup): enroll Secure Boot keys in VM NVRAM after OS installation 2026-04-16 15:37:13 +02:00
c12332a853 feat(virtualization): enable TPM2 emulation for Secure Boot VMs 2026-04-16 15:37:13 +02:00
5ace4577a2 feat(configuration): auto-bind PCR 7 when Secure Boot and FDE are both enabled 2026-04-16 15:37:13 +02:00
c0ff01ef40 feat(configuration): add Secure Boot tasks for shim and sbctl 2026-04-16 15:37:13 +02:00
57417514e3 feat(configuration): override EFI loader to shim when Secure Boot enabled 2026-04-02 04:34:47 +02:00
0928588c1f feat(bootstrap): add Secure Boot conditional packages for Debian, Ubuntu, and Arch 2026-04-02 04:34:16 +02:00
6d622f2db4 feat(global_defaults): add secure_boot feature toggle with normalization 2026-04-02 04:33:07 +02:00
b11d65a6f3 docs(bootstrap): document desktop, initramfs, and FDE features with SSH keepalive config 2026-04-01 15:07:58 +02:00
3623fc292c feat(configuration): generic FDE with systemd-cryptenroll, clevis fallback, and configurable initramfs 2026-04-01 15:07:58 +02:00
dfca7ec94b fix(configuration): RedHat EFI grub wrapper with btrfs subvol prefix and boot order 2026-04-01 15:07:58 +02:00
e8be84bf49 fix(partitioning): set btrfs default subvolume and restrict @pkg to Arch 2026-04-01 15:07:58 +02:00
322cc0b1ce fix(bootstrap): resolve interface-only network, sshd penalties, dnf scriptlets, and EFI cleanup 2026-04-01 15:07:58 +02:00
4b38754f8b feat(bootstrap): add desktop environment support with configurable DE, DM, and display target 2026-04-01 15:07:58 +02:00
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
51 changed files with 1076 additions and 367 deletions

View File

@@ -1,5 +1,6 @@
skip_list:
- run-once
- 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:
- roles/global_defaults/

View File

@@ -202,14 +202,29 @@ When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`
#### `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 |
| ---------- | ----------- | ------- | -------------------------------------------------- |
| `name` | string | -- | Username (required) |
| `password` | string | -- | User password (required for first user) |
| *(dict key)* | string | -- | Username (required) |
| `password` | string | -- | User password (required for at least one user) |
| `keys` | list | `[]` | SSH public keys |
| `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`
@@ -242,7 +257,15 @@ The first user's credentials are prompted interactively via `vars_prompt` unless
| Key | Type | Default | Description |
| -------- | ------------- | ------- | ---------------------------------------------- |
| `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`
@@ -259,6 +282,26 @@ The first user's credentials are prompted interactively via `vars_prompt` unless
| `banner.motd` | bool | `false` | MOTD banner |
| `banner.sudo` | bool | `true` | Sudo banner |
| `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
@@ -398,7 +441,7 @@ ansible-playbook -i inventory.yml main.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:
@@ -408,7 +451,7 @@ Example inventory files are included:
## 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

View File

@@ -3,3 +3,6 @@ hash_behaviour = merge
interpreter_python = auto_silent
deprecation_warnings = False
host_key_checking = False
[ssh_connection]
ssh_args = -C -o ControlMaster=auto -o ControlPersist=600s -o ServerAliveInterval=30 -o ServerAliveCountMax=10

100
main.yml
View File

@@ -14,94 +14,7 @@
strategy: free # noqa: run-once[play]
gather_facts: false
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:
- 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
ansible.builtin.import_role:
name: global_defaults
@@ -160,8 +73,6 @@
ansible.builtin.include_role:
name: cleanup
public: true
vars:
ansible_become: false
rescue:
- name: Delete VM on bootstrap failure
@@ -208,10 +119,15 @@
when:
- post_reboot_can_connect | bool
no_log: true
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact:
ansible_user: "{{ system_cfg.users[0].name }}"
ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_connection: ssh
ansible_host: "{{ system_cfg.network.ip }}"
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_python_interpreter: /usr/bin/python3

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

View File

@@ -18,6 +18,9 @@
groupinstall -y {{ _dnf_groups }}
register: bootstrap_dnf_base_result
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
ansible.builtin.file:

View File

@@ -55,6 +55,11 @@
register: bootstrap_debian_update_result
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
when: bootstrap_debian_extra_args | trim | length > 0
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra_args }}"

View File

@@ -34,6 +34,10 @@
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
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
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf

View File

@@ -54,6 +54,11 @@
register: bootstrap_ubuntu_update_result
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
when: bootstrap_ubuntu_extra_args | trim | length > 0
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra_args }}"

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

View File

@@ -201,6 +201,7 @@ bootstrap_debian:
- lrzsz
- mtr
- ncdu
- needrestart
- net-tools
- network-manager
- python-is-python3
@@ -221,6 +222,7 @@ bootstrap_debian:
+ (['software-properties-common'] if (os_version | string) not in ['13', 'unstable'] else [])
+ (['systemd-zram-generator'] if (os_version | string) not in ['10', '11'] else [])
+ (['tldr'] if (os_version | string) not in ['13', 'unstable'] else [])
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
+ bootstrap_common_conditional
}}
@@ -262,6 +264,7 @@ bootstrap_ubuntu:
- mtr
- ncdu
- ncurses-term
- needrestart
- net-tools
- network-manager
- python-is-python3
@@ -283,6 +286,7 @@ bootstrap_ubuntu:
conditional: >-
{{
(['tldr'] if (os_version | default('') | string | length) > 0 else [])
+ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else [])
+ bootstrap_common_conditional
}}
@@ -321,6 +325,7 @@ bootstrap_archlinux:
{{
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
+ (['iptables-nft'] if system_cfg.features.firewall.toolkit == 'nftables' and system_cfg.features.firewall.enabled | bool else [])
+ (['sbctl'] if system_cfg.features.secure_boot.enabled | bool else [])
+ (bootstrap_common_conditional | reject('equalto', 'nftables') | list)
}}

View File

@@ -14,7 +14,6 @@
- name: Initialize cleaned VM XML
ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_get_xml.get_xml }}"
changed_when: false
- name: Remove boot ISO device from VM XML (source match)
when: boot_iso is defined and boot_iso | length > 0
@@ -28,7 +27,6 @@
when: boot_iso is defined and boot_iso | length > 0
ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot_source.xmlstring }}"
changed_when: false
- name: Remove boot ISO device from VM XML (target fallback)
community.general.xml:
@@ -40,7 +38,6 @@
- name: Update cleaned VM XML after removing boot ISO
ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot.xmlstring }}"
changed_when: false
- name: Remove cloud-init ISO device from VM XML (source match)
community.general.xml:
@@ -52,7 +49,6 @@
- name: Update cleaned VM XML after removing cloud-init ISO source match
ansible.builtin.set_fact:
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)
community.general.xml:
@@ -64,7 +60,6 @@
- name: Update cleaned VM XML after removing cloud-init ISO
ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}"
changed_when: false
- name: Strip XML declaration for libvirt define
ansible.builtin.set_fact:
@@ -76,7 +71,12 @@
| regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "")
| 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
community.libvirt.virt:
@@ -94,6 +94,35 @@
state: destroyed
failed_when: false
- name: Enroll Secure Boot keys in VM NVRAM
when:
- system_cfg.features.secure_boot.enabled | default(false) | bool
- os != 'archlinux'
block:
- name: Find VM NVRAM file path
ansible.builtin.shell:
cmd: >-
set -o pipefail &&
virsh -c {{ libvirt_uri | default('qemu:///system') }} dumpxml {{ hostname }}
| grep -oP '<nvram[^>]*>\K[^<]+'
executable: /bin/bash
register: _sb_nvram_path
changed_when: false
failed_when: false
- name: Enroll Secure Boot keys via virt-fw-vars
when: _sb_nvram_path.stdout | default('') | length > 0
ansible.builtin.command:
argv:
- virt-fw-vars
- --inplace
- "{{ _sb_nvram_path.stdout | trim }}"
- --enroll-redhat
- --secure-boot
register: _sb_enroll_result
changed_when: _sb_enroll_result.rc == 0
failed_when: false
- name: Start the VM
community.libvirt.virt:
name: "{{ hostname }}"

View File

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

View File

@@ -7,34 +7,11 @@
xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}"
block:
- name: Ensure Xen disk definitions exist
when: virtualization_xen_disks is not defined
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
ansible.builtin.include_tasks: ../../virtualization/tasks/_xen_disks.yml
- name: Render Xen VM configuration without installer media
vars:
xen_installer_media_enabled: false
virtualization_xen_disks: "{{ virtualization_xen_disks | default(cleanup_xen_disks | default([])) }}"
ansible.builtin.template:
src: xen.cfg.j2
dest: /tmp/xen-{{ hostname }}.cfg

View File

@@ -14,3 +14,12 @@
- name: Set platform configuration
ansible.builtin.set_fact:
_configuration_platform: "{{ configuration_platform_config[os_family] }}"
- name: Override EFI loader to shim for Secure Boot
when:
- system_cfg.features.secure_boot.enabled | bool
- _configuration_platform.efi_loader != 'shimx64.efi'
- os != 'archlinux'
ansible.builtin.set_fact:
_configuration_platform: >-
{{ _configuration_platform | combine({'efi_loader': 'shimx64.efi'}) }}

View File

@@ -34,6 +34,16 @@
register: configuration_efi_entry_result
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
when: os == "archlinux" and system_cfg.filesystem != "btrfs"
ansible.builtin.lineinfile:
@@ -48,16 +58,62 @@
register: configuration_initramfs_result
changed_when: configuration_initramfs_result.rc == 0
- name: Generate grub config
vars:
configuration_grub_cfg_cmd: >-
{{
'/usr/sbin/' + _configuration_platform.grub_mkconfig_prefix + ' -o '
+ partitioning_efi_mountpoint
+ '/EFI/' + _efi_vendor + '/grub.cfg'
if os_family == 'RedHat'
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
}}
ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}"
- 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:
_grub2_path: >-
{{
'/grub2'
if (partitioning_separate_boot | bool)
else ('/@/boot/grub2' if system_cfg.filesystem == 'btrfs' else '/boot/grub2')
}}
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
changed_when: configuration_grub_result.rc == 0
- name: Rebuild GRUB as standalone EFI for Secure Boot
when:
- system_cfg.features.secure_boot.enabled | default(false) | bool
- os == 'archlinux'
ansible.builtin.command: >-
{{ chroot_command }} grub-mkstandalone
-d /usr/lib/grub/x86_64-efi
-O x86_64-efi
--disable-shim-lock
-o {{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/grubx64.efi
boot/grub/grub.cfg=/boot/grub/grub.cfg
register: _grub_standalone_result
changed_when: _grub_standalone_result.rc == 0

View File

@@ -8,7 +8,7 @@
block:
- name: Set LUKS configuration facts
vars:
luks_tpm2_pcrs: >-
_raw_pcrs: >-
{{
(
system_cfg.luks.tpm2.pcrs
@@ -20,6 +20,17 @@
| regex_replace('\\s+', '')
| regex_replace('^\\+|\\+$', '')
}}
_sb_pcr7_safe: >-
{{
system_cfg.features.secure_boot.enabled | bool
and system_cfg.type | default('virtual') != 'virtual'
}}
luks_tpm2_pcrs: >-
{{
_raw_pcrs
if _raw_pcrs | length > 0
else ('7' if (_sb_pcr7_safe | bool) else '')
}}
ansible.builtin.set_fact:
configuration_luks_mapper_name: "{{ system_cfg.luks.mapper }}"
configuration_luks_uuid: "{{ partitioning_luks_uuid | default('') }}"
@@ -36,6 +47,12 @@
configuration_luks_tpm2_device: "{{ system_cfg.luks.tpm2.device }}"
configuration_luks_tpm2_pcrs: "{{ luks_tpm2_pcrs }}"
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
ansible.builtin.assert:
@@ -51,8 +68,13 @@
fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt.
no_log: true
- name: Enroll TPM2 for LUKS
when: configuration_luks_auto_method == 'tpm2'
- name: Detect TPM2 unlock method
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
- name: Configure LUKS keyfile auto-decrypt
@@ -78,7 +100,7 @@
}}
luks_tpm2_option_list: >-
{{
(configuration_luks_auto_method == 'tpm2')
(configuration_luks_auto_method == 'tpm2' and (_tpm2_method | default('systemd-cryptenroll')) == 'systemd-cryptenroll')
| ternary(
['tpm2-device=' + configuration_luks_tpm2_device]
+ (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
@@ -122,16 +144,16 @@
path: /mnt{{ configuration_luks_keyfile_path }}
state: absent
- name: Configure initramfs for LUKS
ansible.builtin.include_tasks: encryption/initramfs.yml
- name: Configure crypttab
ansible.builtin.include_tasks: encryption/crypttab.yml
- name: Configure initramfs
ansible.builtin.include_tasks: encryption/initramfs.yml
- name: Configure dracut
when: os_family == 'RedHat'
- name: Configure dracut for LUKS
when: _initramfs_generator | default('') == 'dracut'
ansible.builtin.include_tasks: encryption/dracut.yml
- 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

View File

@@ -9,48 +9,58 @@
ansible.builtin.copy:
dest: /mnt/etc/dracut.conf.d/crypt.conf
content: |
add_dracutmodules+=" crypt "
{% if configuration_luks_keyfile_in_use %}
add_dracutmodules+=" crypt systemd "
{% if configuration_luks_keyfile_in_use | default(false) %}
install_items+=" {{ configuration_luks_keyfile_path }} "
{% endif %}
{% if configuration_luks_auto_method == 'tpm2' %}
install_items+=" {{ configuration_luks_tpm2_token_lib | default('') }} "
{% endif %}
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:
src: /mnt/etc/kernel/cmdline
register: configuration_kernel_cmdline_slurp
register: _kernel_cmdline_slurp
failed_when: false
- name: Build kernel cmdline with LUKS args
vars:
kernel_cmdline_current: >-
{{ configuration_kernel_cmdline_slurp.content | b64decode | trim }}
kernel_cmdline_list: >-
_cmdline_current: >-
{{ (_kernel_cmdline_slurp.content | default('') | b64decode | default('')) | trim }}
_cmdline_list: >-
{{ _cmdline_current.split() if _cmdline_current | length > 0 else [] }}
_cmdline_filtered: >-
{{
kernel_cmdline_current.split()
if kernel_cmdline_current | length > 0 else []
}}
kernel_cmdline_filtered: >-
{{
kernel_cmdline_list
_cmdline_list
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
| list
}}
kernel_cmdline_new: >-
_cmdline_new: >-
{{
(kernel_cmdline_filtered + configuration_luks_kernel_args.split())
(_cmdline_filtered + configuration_luks_kernel_args.split())
| unique
| join(' ')
}}
ansible.builtin.set_fact:
configuration_kernel_cmdline_new: "{{ kernel_cmdline_new }}"
_dracut_kernel_cmdline: "{{ _cmdline_new }}"
- name: Write kernel cmdline with LUKS args
ansible.builtin.copy:
dest: /mnt/etc/kernel/cmdline
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
when: os_family == 'RedHat'
vars:
_bls_cmdline: "{{ configuration_kernel_cmdline_new }}"
_bls_cmdline: "{{ _dracut_kernel_cmdline }}"
ansible.builtin.include_tasks: ../_bls_update.yml

View File

@@ -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:
- os_family == 'Debian'
- configuration_luks_keyfile_in_use
- configuration_luks_auto_method == 'tpm2'
- _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:
path: /mnt/etc/cryptsetup-initramfs/conf-hook
regexp: "^KEYFILE_PATTERN="
@@ -10,8 +96,9 @@
create: true
mode: "0644"
# --- mkinitcpio: systemd + sd-encrypt hooks ---
- name: Configure mkinitcpio hooks for LUKS
when: os == 'archlinux'
when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.lineinfile:
path: /mnt/etc/mkinitcpio.conf
regexp: "^HOOKS="
@@ -20,13 +107,13 @@
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
- name: Read mkinitcpio configuration
when: os == 'archlinux'
when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.slurp:
src: /mnt/etc/mkinitcpio.conf
register: configuration_mkinitcpio_slurp
- name: Build mkinitcpio FILES list
when: os == 'archlinux'
when: _initramfs_generator | default('') == 'mkinitcpio'
vars:
mkinitcpio_files_list: >-
{{
@@ -42,7 +129,7 @@
{{
(
(mkinitcpio_files_list + [configuration_luks_keyfile_path])
if configuration_luks_keyfile_in_use
if (configuration_luks_keyfile_in_use | default(false))
else (
mkinitcpio_files_list
| reject('equalto', configuration_luks_keyfile_path)
@@ -55,7 +142,7 @@
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
- name: Configure mkinitcpio FILES list
when: os == 'archlinux'
when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.lineinfile:
path: /mnt/etc/mkinitcpio.conf
regexp: "^FILES="

View File

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

View File

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

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
block:
# Tempfile in chroot /tmp — accessible by both chroot and host commands
- name: Create temporary passphrase file for TPM2 enrollment
ansible.builtin.tempfile:
path: /mnt/tmp
path: /mnt/root
prefix: luks-passphrase-
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:
dest: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
dest: "{{ _tpm2_passphrase_tempfile.path }}"
content: "{{ configuration_luks_passphrase }}"
owner: root
group: root
mode: "0600"
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:
configuration_luks_enroll_args: >-
_enroll_args: >-
{{
[
'/usr/bin/systemd-cryptenroll',
@@ -28,70 +37,28 @@
'--tpm2-with-pin=false',
'--wipe-slot=tpm2',
'--unlock-key-file=' + (
configuration_luks_tpm2_passphrase_tempfile.path
| regex_replace('^/mnt', '')
_tpm2_passphrase_tempfile.path | regex_replace('^/mnt', '')
)
]
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
if configuration_luks_tpm2_pcrs | length > 0 else [])
+ [configuration_luks_device]
}}
configuration_luks_enroll_chroot_cmd: >-
{{ chroot_command }} {{ configuration_luks_enroll_args | join(' ') }}
ansible.builtin.command: "{{ configuration_luks_enroll_chroot_cmd }}"
register: configuration_luks_tpm2_enroll_chroot
changed_when: configuration_luks_tpm2_enroll_chroot.rc == 0
failed_when: false
ansible.builtin.command: "{{ chroot_command }} {{ _enroll_args | join(' ') }}"
register: _tpm2_enroll_result
changed_when: _tpm2_enroll_result.rc == 0
- 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:
- name: Warn about TPM2 enrollment failure
ansible.builtin.fail:
- name: TPM2 enrollment failed
ansible.builtin.debug:
msg: >-
WARNING: TPM2 enrollment failed — falling back to keyfile auto-decrypt.
The system will use a keyfile instead of TPM2 for automatic LUKS unlock.
ignore_errors: true
TPM2 enrollment failed: {{ _tpm2_enroll_result.stderr | default('unknown') }}.
The system will require the passphrase for LUKS unlock on boot.
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:
- name: Remove TPM2 enrollment passphrase file
when: configuration_luks_tpm2_passphrase_tempfile.path is defined
- name: Remove temporary passphrase file
when: _tpm2_passphrase_tempfile.path is defined
ansible.builtin.file:
path: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
path: "{{ _tpm2_passphrase_tempfile.path }}"
state: absent

View File

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

View File

@@ -17,6 +17,8 @@
- file: encryption.yml
when: "{{ system_cfg.luks.enabled | bool }}"
- file: bootloader.yml
- file: secure_boot.yml
when: "{{ system_cfg.features.secure_boot.enabled | bool }}"
- file: extras.yml
- file: network.yml
- file: users.yml

View File

@@ -0,0 +1,8 @@
---
- name: Configure shim-based Secure Boot
when: os != 'archlinux'
ansible.builtin.include_tasks: secure_boot/shim.yml
- name: Configure sbctl Secure Boot
when: os == 'archlinux'
ansible.builtin.include_tasks: secure_boot/sbctl.yml

View File

@@ -0,0 +1,115 @@
---
- name: Configure sbctl Secure Boot
block:
- name: Create Secure Boot signing keys
ansible.builtin.command: "{{ chroot_command }} sbctl create-keys"
register: _sbctl_create_keys
changed_when: _sbctl_create_keys.rc == 0
failed_when:
- _sbctl_create_keys.rc != 0
- "'already exists' not in (_sbctl_create_keys.stderr | default(''))"
- name: Enroll Secure Boot keys in firmware
ansible.builtin.command: "{{ chroot_command }} sbctl enroll-keys --microsoft"
register: _sbctl_enroll
changed_when: _sbctl_enroll.rc == 0
failed_when: false
- name: Install first-boot enrollment service if chroot enrollment failed
when: _sbctl_enroll.rc | default(1) != 0
block:
- name: Create first-boot sbctl enrollment service
ansible.builtin.copy:
dest: /mnt/etc/systemd/system/sbctl-enroll.service
mode: "0644"
content: |
[Unit]
Description=Enroll Secure Boot keys via sbctl
ConditionPathExists=!/var/lib/sbctl/.enrolled
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/bin/sbctl enroll-keys --microsoft
ExecStartPost=/usr/bin/touch /var/lib/sbctl/.enrolled
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
- name: Enable first-boot enrollment service
ansible.builtin.command: "{{ chroot_command }} systemctl enable sbctl-enroll.service"
register: _sbctl_service_enable
changed_when: _sbctl_service_enable.rc == 0
- name: Find kernel images to sign
ansible.builtin.find:
paths: /mnt/boot
patterns: "vmlinuz-*"
file_type: file
register: _sbctl_kernel_images
- name: Sign kernel images
ansible.builtin.command: >-
{{ chroot_command }} sbctl sign -s {{ item.path | regex_replace('^/mnt', '') }}
loop: "{{ _sbctl_kernel_images.files }}"
loop_control:
label: "{{ item.path | basename }}"
register: _sbctl_sign_kernel
changed_when: _sbctl_sign_kernel.rc == 0
failed_when: false
- name: Sign GRUB EFI binary
vars:
_grub_efi_path: "{{ partitioning_efi_mountpoint }}/EFI/archlinux/grubx64.efi"
ansible.builtin.command: >-
{{ chroot_command }} sbctl sign -s {{ _grub_efi_path }}
register: _sbctl_sign_grub
changed_when: _sbctl_sign_grub.rc == 0
failed_when: false
- name: Ensure pacman hooks directory exists
ansible.builtin.file:
path: /mnt/etc/pacman.d/hooks
state: directory
mode: "0755"
- name: Install sbctl auto-signing pacman hook
ansible.builtin.copy:
dest: /mnt/etc/pacman.d/hooks/99-sbctl-sign.hook
mode: "0644"
content: |
[Trigger]
Operation = Install
Operation = Upgrade
Type = Path
Target = boot/vmlinuz-*
Target = usr/lib/modules/*/vmlinuz
[Action]
Description = Signing kernel images for Secure Boot...
When = PostTransaction
Exec = /usr/bin/sbctl sign-all
Depends = sbctl
- name: Verify sbctl signing status
ansible.builtin.command: "{{ chroot_command }} sbctl verify"
register: _sbctl_verify
changed_when: false
failed_when: false
- name: Report sbctl Secure Boot status
ansible.builtin.debug:
msg: >-
Secure Boot (sbctl):
Enrollment={{ 'done' if (_sbctl_enroll.rc | default(1)) == 0 else 'deferred to first boot' }}.
{{ _sbctl_verify.stdout | default('Verify not available') }}
rescue:
- name: Secure Boot setup failed
ansible.builtin.debug:
msg: >-
sbctl Secure Boot setup failed.
On VMs make sure the OVMF firmware is in Setup Mode (fresh NVRAM).
On bare metal enter the firmware setup and switch to Setup Mode first.
To recover manually: sbctl create-keys && sbctl enroll-keys --microsoft && sbctl sign-all

View File

@@ -0,0 +1,45 @@
---
- name: Configure shim-based Secure Boot
vars:
_efi_vendor: >-
{{
"redhat" if os == "rhel"
else ("ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os)
}}
block:
- name: Find shim binary in target system
ansible.builtin.shell:
cmd: >-
set -o pipefail &&
{{ chroot_command }} find /usr/lib/shim /boot/efi/EFI
\( -name 'shimx64.efi.signed.latest' -o -name 'shimx64.efi.dualsigned'
-o -name 'shimx64.efi.signed' -o -name 'shimx64.efi' \)
-type f | sort -r | head -1
executable: /bin/bash
register: _shim_find_result
changed_when: false
failed_when: false
- name: Copy shim to EFI vendor directory
when:
- _shim_find_result.stdout | default('') | length > 0
- _configuration_platform.grub_install | bool
ansible.builtin.command: >-
cp /mnt{{ _shim_find_result.stdout_lines | first }}
/mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/shimx64.efi
register: _shim_copy_result
changed_when: _shim_copy_result.rc == 0
- name: Verify shim is present
ansible.builtin.stat:
path: "/mnt{{ partitioning_efi_mountpoint }}/EFI/{{ _efi_vendor }}/shimx64.efi"
register: _shim_stat
- name: Report Secure Boot status
ansible.builtin.debug:
msg: >-
Secure Boot (shim): {{
'shimx64.efi installed at ' ~ partitioning_efi_mountpoint ~ '/EFI/' ~ _efi_vendor
if (_shim_stat.stat.exists | default(false))
else 'shimx64.efi not found, shim package may handle placement on first boot'
}}

View File

@@ -2,6 +2,12 @@
- name: Enable systemd services
when: _configuration_platform.init_system == 'systemd'
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: >-
{{
['NetworkManager']
@@ -9,12 +15,31 @@
+ (['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 [])
+ (['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 }}"
loop: "{{ configuration_systemd_services }}"
register: configuration_enable_service_result
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
when: _configuration_platform.init_system == 'openrc'
vars:

View File

@@ -15,15 +15,15 @@
validate: /usr/sbin/visudo --check --file=%s
- 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:
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:
content: "{{ item.name }} {{ configuration_sudoers_rule }}\n"
dest: "/mnt/etc/sudoers.d/{{ item.name }}"
content: "{{ item.key }} {{ configuration_sudoers_rule }}\n"
dest: "/mnt/etc/sudoers.d/{{ item.key }}"
mode: "0440"
validate: /usr/sbin/visudo --check --file=%s
loop: "{{ system_cfg.users }}"
loop: "{{ system_cfg.users | dict2items }}"
loop_control:
label: "{{ item.name }}"
label: "{{ item.key }}"

View File

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

View File

@@ -65,3 +65,15 @@ configuration_platform_config:
grub_mkconfig_prefix: grub-mkconfig
locale_gen: false
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

View File

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

View File

@@ -68,6 +68,23 @@
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
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
when:
- not (custom_iso | bool)

View File

@@ -25,6 +25,7 @@
state: latest
loop:
- { name: glibc }
- { name: lua, os: [almalinux, fedora, rhel, rocky] }
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
- { name: debian-archive-keyring, os: [debian] }

View File

@@ -85,7 +85,7 @@ system_defaults:
mirror: ""
packages: []
disks: []
users: []
users: {}
root:
password: ""
shell: "/bin/bash"
@@ -129,8 +129,21 @@ system_defaults:
rhel_repo:
source: "iso" # iso|satellite|none — how RHEL systems get packages post-install
url: "" # Satellite/custom repo URL when source=satellite
aur:
enabled: false
helper: "yay" # yay|paru
user: "_aur_builder"
chroot:
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
secure_boot:
enabled: false
method: "" # arch only: sbctl (default) or uki; ignored for other distros
# Per-hypervisor required fields — drives data-driven validation.
# All virtual types additionally require network bridge or interfaces.

View File

@@ -96,7 +96,7 @@
}}
# --- Storage & accounts ---
disks: "{{ system_raw.disks | default([]) }}"
users: "{{ system_raw.users | default([]) }}"
users: "{{ system_raw.users | default({}) }}"
root:
password: "{{ system_raw.root.password | string }}"
shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}"
@@ -144,27 +144,16 @@
url: "{{ system_raw.features.rhel_repo.url | default('') | string }}"
chroot:
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 }}"
secure_boot:
enabled: "{{ system_raw.features.secure_boot.enabled | bool }}"
method: "{{ system_raw.features.secure_boot.method | default('') | string | lower }}"
hostname: "{{ system_name }}"
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 }}"
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)
}}

View File

@@ -8,11 +8,11 @@
that:
- system 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.luks is not defined or system.luks is mapping
- system.features is not defined or system.features is mapping
fail_msg: "system and its nested keys (network, 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
- name: Validate DNS lists (not strings)
@@ -25,17 +25,17 @@
quiet: true
- 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:
that:
- item is mapping
- item.name is defined and (item.name | string | length) > 0
- item['keys'] is not defined or (item['keys'] is iterable and item['keys'] is not string)
fail_msg: "Each system.users[] entry must be a dict with 'name'; 'keys' must be a list."
- item.value is mapping
- item.key | string | length > 0
- ('keys' not in item.value) or (item.value['keys'] is iterable and item.value['keys'] is not string)
fail_msg: "Each system.users entry must be a dict keyed by username; 'keys' must be a list."
quiet: true
loop: "{{ system.users }}"
loop: "{{ system.users | dict2items }}"
loop_control:
label: "{{ item.name | default('(unnamed)') }}"
label: "{{ item.key }}"
- name: Validate system features input types
when: system.features is defined

View File

@@ -81,10 +81,14 @@
when:
- system_cfg.type == "virtual"
- hypervisor_type != "vmware"
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact:
ansible_user: "{{ system_cfg.users[0].name }}"
ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_host: "{{ system_cfg.network.ip }}"
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"
no_log: true
@@ -92,12 +96,12 @@
when: hypervisor_type == "vmware"
ansible.builtin.set_fact:
ansible_connection: vmware_tools
ansible_vmware_host: "{{ hypervisor_cfg.url }}"
ansible_vmware_port: 443
ansible_host: "{{ hypervisor_cfg.url }}"
ansible_port: 443
ansible_user: root
ansible_password: ""
ansible_vmware_user: "{{ hypervisor_cfg.username }}"
ansible_vmware_password: "{{ hypervisor_cfg.password }}"
ansible_vmware_guest_path: "/{{ hypervisor_cfg.datacenter }}/vm{{ system_cfg.path }}/{{ hostname }}"
ansible_vmware_validate_certs: "{{ hypervisor_cfg.certs | bool }}"
ansible_vmware_tools_user: root
ansible_vmware_tools_password: "{{ system_cfg.root.password }}"
no_log: true

View File

@@ -17,6 +17,27 @@
- name: Normalize disk configuration
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
when: system_cfg is defined
ansible.builtin.set_fact:

View File

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

View File

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

View File

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

View File

@@ -9,12 +9,13 @@
- >-
system_cfg.features.cis.enabled | bool or (
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'])
)
)
- >-
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'
ansible.posix.mount:
path: /mnt{{ item.path }}

View File

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

View File

@@ -43,6 +43,7 @@
when:
- system_cfg.features.cis.enabled | bool or item.subvol not in ['var_log_audit']
- 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 }}
args:
creates: /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
@@ -51,12 +52,18 @@
- { subvol: swap }
- { subvol: home }
- { subvol: var }
- { subvol: pkg }
- { subvol: pkg, os: [archlinux] }
- { subvol: var_log }
- { subvol: var_log_audit }
loop_control:
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
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
args:
creates: /mnt/@swap/swapfile
register: partitioning_btrfs_swap_result
- name: Unmount Partition
ansible.posix.mount:

View File

@@ -22,10 +22,10 @@ virtualization_libvirt_ovmf_vars: /usr/share/edk2/x64/OVMF_VARS.4m.fd
virtualization_tpm2_enabled: >-
{{
(system_cfg.luks.enabled | bool)
and (system_cfg.luks.auto | bool)
and (
(system_cfg.luks.method | lower)
== 'tpm2'
(
(system_cfg.luks.enabled | bool)
and (system_cfg.luks.auto | bool)
and (system_cfg.luks.method | lower == 'tpm2')
)
or (system_cfg.features.secure_boot.enabled | default(false) | bool)
}}

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

@@ -40,10 +40,10 @@
failed_when: false
- name: Undefine libvirt VM
community.libvirt.virt:
name: "{{ hostname }}"
command: undefine
uri: "{{ libvirt_uri | default('qemu:///system') }}"
ansible.builtin.command:
cmd: "virsh -c {{ libvirt_uri | default('qemu:///system') }} undefine {{ hostname }} --nvram"
register: _libvirt_undefine_result
changed_when: _libvirt_undefine_result.rc == 0
failed_when: false
- name: Remove libvirt disk images
@@ -70,6 +70,7 @@
- xl
- destroy
- "{{ hostname }}"
changed_when: false
failed_when: false
- name: Remove Xen VM config

View File

@@ -35,8 +35,8 @@
{%- endfor -%}
{{ out }}
community.proxmox.proxmox_kvm:
ciuser: "{{ system_cfg.users[0].name }}"
cipassword: "{{ system_cfg.users[0].password }}"
ciuser: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).key }}"
cipassword: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).value.password }}"
ciupgrade: false
vmid: "{{ system_cfg.id }}"
name: "{{ hostname }}"

View File

@@ -2,28 +2,7 @@
- name: Deploy VM on Xen
block:
- name: Build disk definitions
ansible.builtin.set_fact:
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
ansible.builtin.include_tasks: _xen_disks.yml
- name: Create VM disks for Xen
delegate_to: localhost

View File

@@ -4,17 +4,22 @@ ssh_pwauth: true
package_update: false
package_upgrade: false
users:
{% for user in system_cfg.users %}
- name: "{{ user.name }}"
primary_group: "{{ user.name }}"
{% for username, attrs in system_cfg.users.items() %}
- name: "{{ username }}"
primary_group: "{{ username }}"
groups: users
{% if attrs.sudo | default(false) | bool %}
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
{% set ssh_keys = user['keys'] | default([]) %}
{% if ssh_keys | length > 0 %}
{% else %}
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:
{% for key in ssh_keys %}
{% for key in attrs['keys'] %}
- "{{ key }}"
{% endfor %}
{% endif %}