Compare commits

..

161 Commits

Author SHA1 Message Date
c73fe22137 fix(bootstrap): use public: true on include_role for cross-role default scoping 2026-03-12 07:15:24 +01:00
868cdea9b2 fix(environment): move SSH switchover into _configure_network.yml before prepare_installer 2026-03-12 06:27:52 +01:00
0c9ccbf28a fix(bootstrap): move SSH switchover to main.yml between roles, guard become with default 2026-03-12 05:57:11 +01:00
225f9967de fix(bootstrap): remove connection override from environment role to enable SSH switchover 2026-03-12 05:50:41 +01:00
7376383324 fix(environment): create /etc/rpm directory before writing macros file 2026-03-12 05:38:10 +01:00
39043b45ee fix(environment): bring interface up before IP assignment, switch to SSH after network config 2026-03-12 05:28:32 +01:00
31b06f1f56 fix(global_defaults): populate flat network fields from interfaces[0] in pre-computed path 2026-03-12 04:49:10 +01:00
6015a9bc4e fix(environment): configure DNS resolvers in /etc/resolv.conf for VMware VMs 2026-03-12 04:02:58 +01:00
4f9893ff6d fix(bootstrap): add pre-block safety flag to prevent deleting pre-existing VMs 2026-03-12 03:41:56 +01:00
ba46c3a157 fix(environment): guard hypervisor_cfg.ssh with default(false) for missing key 2026-03-12 03:37:34 +01:00
4e8dc929a8 fix(bootstrap): replace become directive with ansible_become var on include_role 2026-03-12 03:26:06 +01:00
b59746c463 feat(bootstrap): add rescue block to delete VMs on failed bootstrap 2026-03-12 03:15:15 +01:00
2085fc1696 fix(vmware): add vCenter auth and guest path to vmware_tools connection vars 2026-03-12 02:55:11 +01:00
7227ca1aa0 fix(vmware): inherit vCenter folder from hypervisor_cfg.folder when system.path is empty 2026-03-12 02:41:22 +01:00
a6300a5467 fix(vmware): set vmware_tools connection vars and fix role param precedence 2026-03-12 02:30:03 +01:00
b224f0780e fix(system_check): add become: false to delegated vmware/xen existence checks 2026-03-12 02:14:17 +01:00
c9bc8cb624 fix(global_defaults): handle disks without mount key in mountpoint validation 2026-03-12 01:59:25 +01:00
89ddfedd52 docs: update hypervisor.host references to hypervisor.node 2026-03-11 04:12:48 +01:00
44aa6ac369 feat(vmware): add hypervisor.node as unified placement field (replaces host) 2026-03-11 04:05:36 +01:00
13faf33296 fix(bootstrap): exclude tldr from Ubuntu rolling extra packages 2026-02-22 20:40:46 +01:00
de451be77b fix(global_defaults): use archive.ubuntu.com instead of mirror redirector 2026-02-22 16:26:35 +01:00
af9f264cd3 fix(global_defaults): apply mirror default in pre-computed system_cfg path 2026-02-22 14:20:12 +01:00
750a085e19 feat(configuration): add Debian/Ubuntu repository and apt configuration 2026-02-22 10:47:47 +01:00
f2eb9f2c8e feat(bootstrap): use configurable mirror and write proper sources.list 2026-02-22 10:47:43 +01:00
79988619c6 feat(global_defaults): add system.mirror to schema and normalization 2026-02-22 10:47:40 +01:00
fb69c96e4a chore(bootstrap): update ubuntu non-lts codename to questing (25.10) 2026-02-22 03:08:54 +01:00
d586c087f8 fix(global_defaults): add missing ssh.enabled validation assertion 2026-02-22 03:08:31 +01:00
9dd71b2559 fix(global_defaults): correct fedora version upper bound to 43 2026-02-22 03:08:23 +01:00
35f1702447 feat(global_defaults): add root.shell to system schema and normalization 2026-02-22 03:07:30 +01:00
8b18fbdb4c refactor(cleanup): remove duplicated libvirt path vars, reuse virtualization defaults 2026-02-22 03:07:04 +01:00
909a0a6021 refactor(bootstrap,configuration): rename validation-only _normalize.yml files 2026-02-22 03:06:34 +01:00
2f3fce42b5 fix(partitioning): add | bool to all system_cfg.features.cis.enabled checks 2026-02-22 03:06:13 +01:00
b72816e985 fix(partitioning): add partition separator for NVMe/mmcblk device paths 2026-02-22 02:39:36 +01:00
ac0b5caf83 refactor(configuration): centralize DNS list variables in network dispatch 2026-02-22 02:39:32 +01:00
3ddc3c72ed refactor(configuration): extract shared BLS update task to reduce duplication 2026-02-22 02:39:28 +01:00
f1af7ccbca fix(bootstrap): add missing --best flag to RHEL dnf commands 2026-02-22 02:39:23 +01:00
51ca969ff4 refactor(global_defaults): consolidate hypervisor auth into shared credential dicts 2026-02-22 02:35:04 +01:00
1221249546 refactor(bootstrap,configuration,environment): add defaults/main.yml and extract hardcoded values 2026-02-22 02:32:36 +01:00
87fd69b825 refactor(bootstrap,configuration): add per-role _normalize.yml for platform resolution 2026-02-22 02:27:46 +01:00
3deb3ea751 refactor(configuration): add platform_config dict and replace is_rhel/is_debian with os_family lookups 2026-02-22 02:26:54 +01:00
cc30637f09 feat(global_defaults): add os_family_map and os_family fact for platform config lookups 2026-02-22 02:23:05 +01:00
23721aac96 fix(virtualization): add vTPM2 result validation before VMware power-on 2026-02-22 02:22:37 +01:00
5a9b346d72 feat(global_defaults): add semantic validations for IP, hostname, LUKS method, and interface prefix 2026-02-22 02:22:05 +01:00
75267e5140 refactor(global_defaults): extract physical_default_os to configurable default 2026-02-22 02:21:34 +01:00
f0fb68992d fix(global_defaults): normalize system.type 'vm' to 'virtual' for main project compatibility 2026-02-22 02:21:22 +01:00
0e3edb41f7 docs(bootstrap): add section comments, role boundary docs, and pipeline overview 2026-02-22 01:59:12 +01:00
2bf0cb901e refactor(global_defaults): data-driven hypervisor validation and shared constants 2026-02-22 01:59:09 +01:00
1216c79619 refactor(extras): convert custom.sh from template to static copy 2026-02-22 01:59:04 +01:00
4efd64664d fix(cleanup,config): xen tmp cleanup, tpm2 fallback warning, add code comments 2026-02-22 01:59:01 +01:00
dc5aa5077e fix(partitioning,network): swapon idempotency, DNS search domains, tune2fs changed_when 2026-02-22 01:58:56 +01:00
c65934c290 fix(encryption): add no_log to LUKS configuration block 2026-02-22 01:58:52 +01:00
5b8438ac3b fix(network): bind NM connections to detected interface names for multi-NIC 2026-02-21 16:51:15 +01:00
45df803131 fix(bootstrap): make dhcp-client conditional for EL < 10 (removed in EL 10) 2026-02-21 13:43:41 +01:00
30f74fa4bd fix(bootstrap): remove --asexplicit from pacstrap to preserve dependency metadata 2026-02-21 13:26:59 +01:00
19372db27e fix(bootstrap): add kernel package to rocky and almalinux extra packages 2026-02-21 12:06:09 +01:00
d55fc5799d fix(bootstrap): detect kernel package name for dnf family reinstall step 2026-02-21 11:46:57 +01:00
98231be0bd fix(bootstrap): ensure chroot DNS resolution before installing extra packages 2026-02-21 11:30:28 +01:00
c46a4a5a0a fix(environment): align repo IDs in rocky and almalinux templates with bootstrap config 2026-02-21 11:18:34 +01:00
b84688f1d6 fix(configuration): omit interface-name when not explicitly provided to avoid predictable naming mismatch 2026-02-21 08:29:24 +01:00
b1d2294d63 refactor(configuration): rename _uid to configuration_uid for role prefix convention 2026-02-21 05:14:33 +01:00
ac339b54c4 fix(configuration): handle boolean sudo values in sudoers deployment 2026-02-21 05:14:29 +01:00
cb46a6989f fix(configuration): use full path for chpasswd in chroot 2026-02-21 05:03:36 +01:00
73ea7a177b fix(global_defaults): enrich pre-computed system_cfg with bootstrap defaults 2026-02-21 04:24:23 +01:00
0f8faf0a22 chore(lint): suppress var-naming for user-facing API dicts 2026-02-21 02:58:10 +01:00
b520126253 fix(configuration): remove unnecessary changed_when on set_fact tasks 2026-02-21 02:56:58 +01:00
a4ca4c4ff4 refactor(cis): align normalization with main project activation gate pattern 2026-02-21 02:56:39 +01:00
d9efb54bec fix(global_defaults): remove dead /swap and make pacman cache arch-only in reserved mounts 2026-02-21 02:56:20 +01:00
e7a0cc4f62 fix(global_defaults): set filesystem default to ext4 instead of empty string 2026-02-21 02:56:08 +01:00
a76f317f8f refactor(bootstrap): restructure package lists to self-contained per-OS dicts with base/extra/conditional 2026-02-21 02:39:06 +01:00
e5bd152fb3 refactor(environment): split main.yml into focused sub-task files 2026-02-21 02:39:05 +01:00
6d1c3577df refactor(global_defaults): add idempotency guards to normalization tasks 2026-02-21 02:39:03 +01:00
86f0284acb fix(global_defaults): default interface name to eth0 instead of empty string 2026-02-21 02:38:59 +01:00
221bb4d517 docs(cis): add comment explaining squashfs/snap Ubuntu exclusion 2026-02-21 02:38:58 +01:00
e81ba76446 chore(bootstrap): pin collection versions in requirements.yml 2026-02-21 02:38:57 +01:00
54bbb9d15c fix(bootstrap): move Jinja to end of task name and rename registers to bootstrap_dnf_* 2026-02-21 02:38:27 +01:00
f94b220020 docs: update README with cis dict API, execution pipeline, and cleanup defaults 2026-02-21 01:30:36 +01:00
3fd470d63e fix(validation): align btrfs disk size check with new 2GB swap minimum 2026-02-21 01:28:32 +01:00
a3cd507b2a refactor(bootstrap): unify rocky, almalinux, and fedora into shared _dnf_family.yml 2026-02-21 01:27:33 +01:00
f74ec325ea refactor(cis): extract hardcoded values to cis_defaults and add _normalize.yml 2026-02-21 01:26:31 +01:00
bef15af69f refactor(cleanup): prioritize source-match over target-match in libvirt media removal 2026-02-21 01:22:44 +01:00
7970d933e8 docs(cis): explain Fedora exclusion from crypto-policy configuration 2026-02-21 01:22:41 +01:00
a123a32feb fix(bootstrap): replace brittle sed with ansible.builtin.replace for ubuntu universe repo 2026-02-21 01:22:37 +01:00
54c704de4e refactor(virtualization): simplify cloud-user-data sudo to unconditional NOPASSWD 2026-02-21 01:22:34 +01:00
9308d09d7b fix(bootstrap): remove duplicate lrzsz and gate dbus-daemon on version in almalinux 2026-02-21 01:20:34 +01:00
f367844239 fix(virtualization): fix cloud-user-data sudo logic to respect sudo: false 2026-02-21 01:20:31 +01:00
53e4499d2b fix(partitioning): lower swap minimum from 4GB to 2GB for small VMs 2026-02-21 01:19:23 +01:00
eb63a4fa83 fix(partitioning): add wipefs before mkfs on extra disk partitions 2026-02-21 01:19:19 +01:00
9e3688ae2b fix(cis): strengthen kernel module blacklist and sysctl hardening 2026-02-21 01:18:52 +01:00
dea01cc8a0 refactor(partitioning): split monolithic main.yml into focused task files 2026-02-21 00:39:03 +01:00
92c9702e1d fix(validation): add CIDR prefix range check and Ubuntu version validation 2026-02-21 00:38:57 +01:00
c837a52a24 refactor(cis): remove redundant AllowUsers/AllowGroups/DenyUsers/DenyGroups from sshd 2026-02-21 00:38:52 +01:00
fbd57e0603 fix(cis): skip squashfs blacklist on Ubuntu to preserve snap functionality 2026-02-21 00:38:47 +01:00
40a9ee9882 fix(partitioning): correct changed_when on btrfs quota and qgroup commands 2026-02-21 00:38:43 +01:00
3448e95e5c fix(cis): add regexp to all lineinfile entries in security_lines.yml for idempotency 2026-02-21 00:38:36 +01:00
074831833f fix: add no_log to credential-handling pre_tasks and post_tasks in main.yml 2026-02-21 00:38:32 +01:00
d1a5217e88 fix(virtualization): add no_log and secure temp file handling to libvirt cloud-init 2026-02-21 00:38:28 +01:00
07492b5b57 refactor(cleanup): add configurable verify_boot, boot_timeout, and remove_on_failure defaults 2026-02-20 23:02:24 +01:00
14913bcd3d refactor: move playbook-root templates into their respective roles 2026-02-20 23:01:38 +01:00
041650c287 refactor: add loop_control labels to dict-based loops across all roles 2026-02-20 23:00:53 +01:00
a63ffbc731 refactor(partitioning): move btrfs home quota to configurable default 2026-02-20 22:55:37 +01:00
9d2f1cc5bd fix(environment): detect RHEL ISO device dynamically instead of hardcoded /dev/sr paths 2026-02-20 22:54:42 +01:00
f72f9feb9a refactor(global_defaults): split system.yml into composable normalization stages 2026-02-20 22:54:05 +01:00
417737f904 refactor(global_defaults): extract OS family lists to single source of truth 2026-02-20 22:52:55 +01:00
a06c2ebdcf fix(partitioning): add failed_when to all blkid commands to catch empty UUIDs 2026-02-20 22:52:18 +01:00
e174ecda42 fix(partitioning): add default fallbacks for is_rhel, os, os_version in defaults 2026-02-20 22:51:37 +01:00
5246a905bb fix(virtualization): use hostname variable instead of hardcoded archiso in cloud-user-data 2026-02-20 22:51:32 +01:00
d00d84b69c fix(virtualization): avoid no-handler lint finding in xen VM created tracking 2026-02-20 22:29:03 +01:00
4dafa8c596 fix(partitioning): fix line length violation in home size calculation 2026-02-20 22:28:58 +01:00
53584b8730 fix(configuration): add pipefail to root password shell pipe 2026-02-20 22:28:54 +01:00
ce40468b77 fix(bootstrap): use release map for ubuntu version detection 2026-02-20 22:27:46 +01:00
4b4fab3c33 chore: add .yamllint matching main project conventions 2026-02-20 22:27:31 +01:00
db2fab5e7d fix(configuration): use chpasswd for root password and separate shell setting 2026-02-20 22:27:17 +01:00
42be0a5919 fix(configuration): add explicit LUKS auto-decrypt fallback state tracking and logging 2026-02-20 22:26:47 +01:00
17400fa6ff refactor(partitioning): externalize hardcoded LVM and disk sizing constants to defaults 2026-02-20 22:26:23 +01:00
deb14d2c94 fix(virtualization): add xen VM existence check and improve changed_when 2026-02-20 22:25:10 +01:00
65c5b1029b fix(cis): add pipefail to sshd version detection and define binary defaults 2026-02-20 22:24:14 +01:00
a1fbb7c21d feat(cleanup): gate RHEL ISO disk and fstab handling on rhel_repo.source 2026-02-20 21:51:20 +01:00
d076ac8fef feat(global_defaults): add system.features.rhel_repo option (iso|satellite|none) 2026-02-20 21:51:16 +01:00
c82e4afc4d fix(encryption): add warning before silent TPM2-to-keyfile fallback 2026-02-20 21:51:12 +01:00
ac72fdc4a6 fix(partitioning): correct wipefs changed_when to report actual disk modification 2026-02-20 21:51:09 +01:00
b2e050c467 fix(validation): require password for primary user in system.users[0] 2026-02-20 21:51:06 +01:00
914d7dd9d1 fix(system_check): move no_log from block to individual API tasks 2026-02-20 21:51:02 +01:00
21bf8f79e2 fix(cis): make mlkem768x25519-sha256 KexAlgorithm conditional on OpenSSH 9.9+ 2026-02-20 21:50:58 +01:00
38feff4369 fix(cis): use is_rhel for journald config path instead of fedora-only check 2026-02-20 21:50:55 +01:00
404529e8a4 refactor(configuration): add conditional dispatch to task includes 2026-02-20 21:16:52 +01:00
3db18858c3 refactor(cis): move OS-specific binary resolution to vars/main.yml 2026-02-20 21:16:48 +01:00
72a9576abe refactor(configuration): split network.yml into per-init-system dispatch files 2026-02-20 21:16:45 +01:00
462c2c7dfe refactor(bootstrap): restructure conditional package lists to list concatenation 2026-02-20 21:16:40 +01:00
ef8bfeaf84 refactor(configuration): convert services.yml to list-based loop 2026-02-20 21:16:37 +01:00
ba6be037ac refactor(virt): adopt module_defaults for hypervisor credentials 2026-02-20 21:16:33 +01:00
5ca1c7f570 refactor(cleanup): restructure dispatch to use hypervisor_type include 2026-02-20 21:16:28 +01:00
cd8e477534 refactor(partitioning): extract VG name to defaults variable 2026-02-20 21:16:25 +01:00
c439e9741e fix(configuration): remove trailing blank line from extras.yml 2026-02-20 20:20:33 +01:00
0a5c70e49f docs(environment): document RPM GPG policy relaxation 2026-02-20 20:19:57 +01:00
19f2c9efe2 chore(bootstrap): align ansible.cfg with main project settings 2026-02-20 20:19:46 +01:00
230c74fd9b feat(system_check): add safety check for physical installs 2026-02-20 20:19:37 +01:00
a2c19e2e49 fix(cleanup): fix vmware CD-ROM omit fragility and add cross-role defaults 2026-02-20 20:19:25 +01:00
9f9a4b38b8 fix(virtualization): add XML safety attributes and switch xen to virtio 2026-02-20 20:18:49 +01:00
524356cf8d fix(cis): remove deprecated sshd options and update hardening values 2026-02-20 20:17:52 +01:00
a2993212ca fix(configuration): disambiguate BLS task names and clean up misc noise 2026-02-20 20:17:05 +01:00
fba2e5fc94 refactor(configuration): relocate login banner and fix blockinfile markers 2026-02-20 20:16:19 +01:00
cf68a93b45 fix(configuration): use short hostname and allow per-user shell 2026-02-20 20:15:49 +01:00
3000268a0e fix(partitioning): mount extra disks by UUID instead of device path 2026-02-20 20:15:25 +01:00
196c5be67a fix(partitioning): correct LVM swap sizing and harden UUID fallbacks 2026-02-20 20:15:00 +01:00
33bad193b4 fix(configuration): add trailing semicolons to NM keyfile DNS fields 2026-02-20 20:14:06 +01:00
d5277802f7 fix(bootstrap): add missing packages and remove duplicates 2026-02-20 20:13:53 +01:00
28e6cf50d1 fix(bootstrap): add devpts mount and use ephemeral state for RHEL DVD 2026-02-20 20:12:59 +01:00
42cb5071c2 fix(bootstrap): unify resolv.conf to live environment DNS symlink 2026-02-20 20:12:42 +01:00
23a798a63a fix(global_defaults): add no_log to hypervisor tasks and expand validation 2026-02-20 20:11:37 +01:00
5dd84c6b39 fix: configurable OVMF/machine type, routes syntax, package lists, interface names 2026-02-20 18:47:12 +01:00
d0ae20911b fix(cleanup): keep RHEL ISO ide1 attached as local repo 2026-02-20 18:41:40 +01:00
b6d06dd96d fix: deep analysis audit — no_log, resolv.conf, service conflicts, lint 2026-02-20 18:34:59 +01:00
09b3ed44ba fix(bootstrap): RHEL 9 bootstrap from Arch ISO compatibility
- Generate resolv.conf from inventory DNS settings instead of copying
  host file (Arch ISO has systemd-resolved stub 127.0.0.53)
- Add XFS compat options for GRUB 2.06 and kernel 5.14 across LVM
  volumes, /boot partition, and data disks
- Mount API filesystems (proc, sys, dev) into chroot for RPM scriptlets
- Bypass GPG Sequoia validation with _pkgverify_level none
- Tolerate grub2-common scriptlet warnings
- Handle libvirt VM destroy gracefully during cleanup
2026-02-20 16:58:59 +01:00
603abe63cb refactor: make bootstrap host target configurable 2026-02-20 16:58:59 +01:00
1c0e6533ae fix(ubuntu): add initramfs-tools to debootstrap base packages 2026-02-20 16:58:59 +01:00
00aa614cfd fix(bootstrap): use explicit keyring for debootstrap and copy resolv.conf 2026-02-20 16:58:59 +01:00
4905d10bc0 fix(cloud-init): handle boolean sudo values in user-data template 2026-02-20 16:58:59 +01:00
b4e8ccb77f fix: re-gather facts after reboot to detect target OS package manager
The live ISO (Arch) caches ansible_pkg_mgr=pacman. After rebooting
into the target OS (e.g. Debian), package module fails because pacman
is not available. Re-gather minimal facts including pkg_mgr.
2026-02-20 16:58:59 +01:00
2a82ee4d5c fix: resolve Jinja2 .keys ambiguity, fastfetch availability, and python interpreter
- Use bracket notation item['keys'] instead of item.keys to avoid
  conflict with Python dict .keys() method
- Remove fastfetch from Debian 12 package list (only available in 13+)
- Set explicit python interpreter path for post-reboot tasks
2026-02-20 16:58:58 +01:00
7b213e7456 fix(partitioning): create separate /boot for LVM-based filesystems
VMware EFI firmware may not initialize all SCSI devices before GRUB
runs, preventing LVM assembly when the root LV spans multiple disks.
A separate /boot partition (the standard RHEL Anaconda layout) lets
GRUB load kernels without LVM; the kernel initramfs handles LVM
activation with proper device waiting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 04:50:32 +01:00
cfc261878a fix(bootloader): run efibootmgr on host for universal chroot compatibility
The previous approach ran efibootmgr inside the chroot, which only works
with arch-chroot (auto-mounts efivars) but fails silently with
systemd-nspawn or plain chroot. Move EFI boot entry creation to the host
where efivars is always available.

Also fixes wrong EFI loader path (\efi\EFI\... -> \EFI\...) and uses
the correct vendor label (e.g. "redhat" instead of raw os variable).

For non-RHEL distros, grub-install now uses --no-nvram to avoid
redundant NVRAM writes; the host efibootmgr handles entry creation
for all distros uniformly with idempotent pre-check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 03:36:20 +01:00
53 changed files with 541 additions and 1255 deletions

View File

@@ -1,6 +1,5 @@
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,29 +202,14 @@ 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 |
| ---------- | ----------- | ------- | -------------------------------------------------- |
| *(dict key)* | string | -- | Username (required) |
| `password` | string | -- | User password (required for at least one user) |
| `name` | string | -- | Username (required) |
| `password` | string | -- | User password (required for first user) |
| `keys` | list | `[]` | SSH public keys |
| `sudo` | bool/string | -- | `true` for NOPASSWD ALL, or custom sudoers string |
Users must be defined in inventory. The dict format enables additive merging across inventory layers with `hash_behaviour=merge`.
The first user's credentials are prompted interactively via `vars_prompt` unless supplied in inventory or `-e`.
#### `system.root`
@@ -257,15 +242,7 @@ Users must be defined in inventory. The dict format enables additive merging acr
| Key | Type | Default | Description |
| -------- | ------------- | ------- | ---------------------------------------------- |
| `device` | string | `auto` | TPM2 device selector |
| `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`.
| `pcrs` | string/list | -- | PCR binding policy (e.g. `"7"` or `"0+7"`) |
#### `system.features`
@@ -282,26 +259,6 @@ The bootstrap auto-switches to dracut when `method: tpm2` is set. Override via `
| `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
@@ -441,7 +398,7 @@ ansible-playbook -i inventory.yml main.yml
ansible-playbook -i inventory.yml main.yml -e @vars.yml
```
All credentials (`system.users`, `system.root.password`) must be defined in inventory or passed via `-e`.
Credentials for the first user and root are prompted interactively via `vars_prompt` unless already set in inventory or passed via `-e`.
Example inventory files are included:
@@ -451,7 +408,7 @@ Example inventory files are included:
## 7. Security
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, user passwords in `system.users`, `system.root.password`).
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, `system.users[].password`, `system.root.password`).
## 8. Safety

View File

@@ -3,6 +3,3 @@ 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,7 +14,94 @@
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
@@ -73,6 +160,8 @@
ansible.builtin.include_role:
name: cleanup
public: true
vars:
ansible_become: false
rescue:
- name: Delete VM on bootstrap failure
@@ -119,15 +208,10 @@
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_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_user: "{{ system_cfg.users[0].name }}"
ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
ansible_python_interpreter: /usr/bin/python3

View File

@@ -1,48 +0,0 @@
---
- 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,9 +18,6 @@
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,11 +55,6 @@
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,10 +34,6 @@
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,11 +54,6 @@
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

@@ -1,149 +0,0 @@
---
# 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,7 +201,6 @@ bootstrap_debian:
- lrzsz
- mtr
- ncdu
- needrestart
- net-tools
- network-manager
- python-is-python3
@@ -222,7 +221,6 @@ 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
}}
@@ -264,7 +262,6 @@ bootstrap_ubuntu:
- mtr
- ncdu
- ncurses-term
- needrestart
- net-tools
- network-manager
- python-is-python3
@@ -286,7 +283,6 @@ 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
}}
@@ -325,7 +321,6 @@ 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,6 +14,7 @@
- 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
@@ -27,6 +28,7 @@
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:
@@ -38,6 +40,7 @@
- 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:
@@ -49,6 +52,7 @@
- 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:
@@ -60,6 +64,7 @@
- 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:
@@ -71,12 +76,7 @@
| regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "")
| trim
}}
- 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"/>') }}
changed_when: false
- name: Update VM definition without installer media
community.libvirt.virt:
@@ -94,35 +94,6 @@
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,4 +25,3 @@
community.proxmox.proxmox_kvm:
vmid: "{{ system_cfg.id }}"
state: restarted
no_log: true

View File

@@ -7,11 +7,34 @@
xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}"
block:
- name: Ensure Xen disk definitions exist
ansible.builtin.include_tasks: ../../virtualization/tasks/_xen_disks.yml
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
- 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,12 +14,3 @@
- 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,16 +34,6 @@
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:
@@ -58,62 +48,16 @@
register: configuration_initramfs_result
changed_when: configuration_initramfs_result.rc == 0
- 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'
- name: Generate grub config
vars:
_grub2_path: >-
configuration_grub_cfg_cmd: >-
{{
'/grub2'
if (partitioning_separate_boot | bool)
else ('/@/boot/grub2' if system_cfg.filesystem == 'btrfs' else '/boot/grub2')
'/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.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"
ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}"
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:
_raw_pcrs: >-
luks_tpm2_pcrs: >-
{{
(
system_cfg.luks.tpm2.pcrs
@@ -20,17 +20,6 @@
| 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('') }}"
@@ -47,12 +36,6 @@
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:
@@ -68,13 +51,8 @@
fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt.
no_log: true
- 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'
- name: Enroll TPM2 for LUKS
when: configuration_luks_auto_method == 'tpm2'
ansible.builtin.include_tasks: encryption/tpm2.yml
- name: Configure LUKS keyfile auto-decrypt
@@ -100,7 +78,7 @@
}}
luks_tpm2_option_list: >-
{{
(configuration_luks_auto_method == 'tpm2' and (_tpm2_method | default('systemd-cryptenroll')) == 'systemd-cryptenroll')
(configuration_luks_auto_method == 'tpm2')
| ternary(
['tpm2-device=' + configuration_luks_tpm2_device]
+ (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
@@ -144,16 +122,216 @@
path: /mnt{{ configuration_luks_keyfile_path }}
state: absent
- name: Configure initramfs for LUKS
ansible.builtin.include_tasks: encryption/initramfs.yml
- name: Write crypttab entry
ansible.builtin.lineinfile:
path: /mnt/etc/crypttab
regexp: "^{{ configuration_luks_mapper_name }}\\s"
line: >-
{{ configuration_luks_mapper_name }} UUID={{ configuration_luks_uuid }}
{{ configuration_luks_crypttab_keyfile }} {{ configuration_luks_crypttab_options }}
create: true
mode: "0600"
- name: Configure crypttab
ansible.builtin.include_tasks: encryption/crypttab.yml
- name: Ensure keyfile pattern for initramfs-tools
when:
- os_family == 'Debian'
- configuration_luks_keyfile_in_use
ansible.builtin.lineinfile:
path: /mnt/etc/cryptsetup-initramfs/conf-hook
regexp: "^KEYFILE_PATTERN="
line: "KEYFILE_PATTERN=/etc/cryptsetup-keys.d/*.key"
create: true
mode: "0644"
- name: Configure mkinitcpio hooks for LUKS
when: os == 'archlinux'
ansible.builtin.lineinfile:
path: /mnt/etc/mkinitcpio.conf
regexp: "^HOOKS="
line: >-
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
- name: Read mkinitcpio configuration
when: os == 'archlinux'
ansible.builtin.slurp:
src: /mnt/etc/mkinitcpio.conf
register: configuration_mkinitcpio_slurp
- name: Build mkinitcpio FILES list
when: os == 'archlinux'
vars:
mkinitcpio_files_list: >-
{{
(
configuration_mkinitcpio_slurp.content | b64decode
| regex_findall('^FILES=\\(([^)]*)\\)', multiline=True)
| default([])
| first
| default('')
).split()
}}
mkinitcpio_files_list_new: >-
{{
(
(mkinitcpio_files_list + [configuration_luks_keyfile_path])
if configuration_luks_keyfile_in_use
else (
mkinitcpio_files_list
| reject('equalto', configuration_luks_keyfile_path)
| list
)
)
| unique
}}
ansible.builtin.set_fact:
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
- name: Configure mkinitcpio FILES list
when: os == 'archlinux'
ansible.builtin.lineinfile:
path: /mnt/etc/mkinitcpio.conf
regexp: "^FILES="
line: >-
FILES=({{
configuration_mkinitcpio_files_list_new | join(' ')
}})
- name: Ensure dracut config directory exists
when: os_family == 'RedHat'
ansible.builtin.file:
path: /mnt/etc/dracut.conf.d
state: directory
mode: "0755"
- name: Configure dracut for LUKS
when: _initramfs_generator | default('') == 'dracut'
ansible.builtin.include_tasks: encryption/dracut.yml
when: os_family == 'RedHat'
ansible.builtin.copy:
dest: /mnt/etc/dracut.conf.d/crypt.conf
content: |
add_dracutmodules+=" crypt "
{% if configuration_luks_keyfile_in_use %}
install_items+=" {{ configuration_luks_keyfile_path }} "
{% endif %}
mode: "0644"
- name: Configure GRUB for LUKS
when: _initramfs_generator | default('') != 'dracut' or os_family != 'RedHat'
ansible.builtin.include_tasks: encryption/grub.yml
- name: Read kernel cmdline defaults
when: os_family == 'RedHat'
ansible.builtin.slurp:
src: /mnt/etc/kernel/cmdline
register: configuration_kernel_cmdline_slurp
- name: Build kernel cmdline with LUKS args
when: os_family == 'RedHat'
vars:
kernel_cmdline_current: >-
{{ configuration_kernel_cmdline_slurp.content | b64decode | trim }}
kernel_cmdline_list: >-
{{
kernel_cmdline_current.split()
if kernel_cmdline_current | length > 0 else []
}}
kernel_cmdline_filtered: >-
{{
kernel_cmdline_list
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
| list
}}
kernel_cmdline_new: >-
{{
(kernel_cmdline_filtered + configuration_luks_kernel_args.split())
| unique
| join(' ')
}}
ansible.builtin.set_fact:
configuration_kernel_cmdline_new: "{{ kernel_cmdline_new }}"
- name: Write kernel cmdline with LUKS args
when: os_family == 'RedHat'
ansible.builtin.copy:
dest: /mnt/etc/kernel/cmdline
mode: "0644"
content: "{{ configuration_kernel_cmdline_new }}\n"
- name: Update BLS entries with LUKS kernel cmdline
when: os_family == 'RedHat'
vars:
_bls_cmdline: "{{ configuration_kernel_cmdline_new }}"
ansible.builtin.include_tasks: _bls_update.yml
- name: Read grub defaults
when: not os_family == 'RedHat'
ansible.builtin.slurp:
src: /mnt/etc/default/grub
register: configuration_grub_slurp
- name: Build grub command lines with LUKS args
when: not os_family == 'RedHat'
vars:
grub_content: "{{ configuration_grub_slurp.content | b64decode }}"
grub_cmdline_linux: >-
{{
grub_content
| regex_findall('^GRUB_CMDLINE_LINUX=\"(.*)\"', multiline=True)
| default([])
| first
| default('')
}}
grub_cmdline_default: >-
{{
grub_content
| regex_findall('^GRUB_CMDLINE_LINUX_DEFAULT=\"(.*)\"', multiline=True)
| default([])
| first
| default('')
}}
grub_cmdline_linux_list: >-
{{
grub_cmdline_linux.split()
if grub_cmdline_linux | length > 0 else []
}}
grub_cmdline_default_list: >-
{{
grub_cmdline_default.split()
if grub_cmdline_default | length > 0 else []
}}
luks_kernel_args_list: "{{ configuration_luks_kernel_args.split() }}"
grub_cmdline_linux_new: >-
{{
(
(
grub_cmdline_linux_list
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
| list
)
+ luks_kernel_args_list
)
| unique
| join(' ')
}}
grub_cmdline_default_new: >-
{{
(
(
grub_cmdline_default_list
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
| list
)
+ luks_kernel_args_list
)
| unique
| join(' ')
}}
ansible.builtin.set_fact:
configuration_grub_content: "{{ grub_content }}"
configuration_grub_cmdline_linux: "{{ grub_cmdline_linux }}"
configuration_grub_cmdline_default: "{{ grub_cmdline_default }}"
configuration_grub_cmdline_linux_new: "{{ grub_cmdline_linux_new }}"
configuration_grub_cmdline_default_new: "{{ grub_cmdline_default_new }}"
- name: Update GRUB_CMDLINE_LINUX_DEFAULT for LUKS
when: not os_family == 'RedHat'
ansible.builtin.lineinfile:
path: /mnt/etc/default/grub
regexp: "^GRUB_CMDLINE_LINUX_DEFAULT="
line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ configuration_grub_cmdline_default_new }}"'

View File

@@ -1,10 +0,0 @@
---
- name: Write crypttab entry
ansible.builtin.lineinfile:
path: /mnt/etc/crypttab
regexp: "^{{ configuration_luks_mapper_name }}\\s"
line: >-
{{ configuration_luks_mapper_name }} UUID={{ configuration_luks_uuid }}
{{ configuration_luks_crypttab_keyfile }} {{ configuration_luks_crypttab_options }}
create: true
mode: "0600"

View File

@@ -1,66 +0,0 @@
---
- name: Ensure dracut config directory exists
ansible.builtin.file:
path: /mnt/etc/dracut.conf.d
state: directory
mode: "0755"
- name: Configure dracut for LUKS
ansible.builtin.copy:
dest: /mnt/etc/dracut.conf.d/crypt.conf
content: |
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"
# --- 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: _kernel_cmdline_slurp
failed_when: false
- name: Build kernel cmdline with LUKS args
vars:
_cmdline_current: >-
{{ (_kernel_cmdline_slurp.content | default('') | b64decode | default('')) | trim }}
_cmdline_list: >-
{{ _cmdline_current.split() if _cmdline_current | length > 0 else [] }}
_cmdline_filtered: >-
{{
_cmdline_list
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
| list
}}
_cmdline_new: >-
{{
(_cmdline_filtered + configuration_luks_kernel_args.split())
| unique
| join(' ')
}}
ansible.builtin.set_fact:
_dracut_kernel_cmdline: "{{ _cmdline_new }}"
- name: Write kernel cmdline with LUKS args
ansible.builtin.copy:
dest: /mnt/etc/kernel/cmdline
mode: "0644"
content: "{{ _dracut_kernel_cmdline }}\n"
# --- BLS entries: RedHat-specific ---
- name: Update BLS entries with LUKS kernel cmdline
when: os_family == 'RedHat'
vars:
_bls_cmdline: "{{ _dracut_kernel_cmdline }}"
ansible.builtin.include_tasks: ../_bls_update.yml

View File

@@ -1,74 +0,0 @@
---
- name: Read grub defaults
ansible.builtin.slurp:
src: /mnt/etc/default/grub
register: configuration_grub_slurp
- name: Build grub command lines with LUKS args
vars:
grub_content: "{{ configuration_grub_slurp.content | b64decode }}"
grub_cmdline_linux: >-
{{
grub_content
| regex_findall('^GRUB_CMDLINE_LINUX=\"(.*)\"', multiline=True)
| default([])
| first
| default('')
}}
grub_cmdline_default: >-
{{
grub_content
| regex_findall('^GRUB_CMDLINE_LINUX_DEFAULT=\"(.*)\"', multiline=True)
| default([])
| first
| default('')
}}
grub_cmdline_linux_list: >-
{{
grub_cmdline_linux.split()
if grub_cmdline_linux | length > 0 else []
}}
grub_cmdline_default_list: >-
{{
grub_cmdline_default.split()
if grub_cmdline_default | length > 0 else []
}}
luks_kernel_args_list: "{{ configuration_luks_kernel_args.split() }}"
grub_cmdline_linux_new: >-
{{
(
(
grub_cmdline_linux_list
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
| list
)
+ luks_kernel_args_list
)
| unique
| join(' ')
}}
grub_cmdline_default_new: >-
{{
(
(
grub_cmdline_default_list
| reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=')
| list
)
+ luks_kernel_args_list
)
| unique
| join(' ')
}}
ansible.builtin.set_fact:
configuration_grub_content: "{{ grub_content }}"
configuration_grub_cmdline_linux: "{{ grub_cmdline_linux }}"
configuration_grub_cmdline_default: "{{ grub_cmdline_default }}"
configuration_grub_cmdline_linux_new: "{{ grub_cmdline_linux_new }}"
configuration_grub_cmdline_default_new: "{{ grub_cmdline_default_new }}"
- name: Update GRUB_CMDLINE_LINUX_DEFAULT for LUKS
ansible.builtin.lineinfile:
path: /mnt/etc/default/grub
regexp: "^GRUB_CMDLINE_LINUX_DEFAULT="
line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ configuration_grub_cmdline_default_new }}"'

View File

@@ -1,152 +0,0 @@
---
# 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:
- 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="
line: "KEYFILE_PATTERN=/etc/cryptsetup-keys.d/*.key"
create: true
mode: "0644"
# --- mkinitcpio: systemd + sd-encrypt hooks ---
- name: Configure mkinitcpio hooks for LUKS
when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.lineinfile:
path: /mnt/etc/mkinitcpio.conf
regexp: "^HOOKS="
line: >-
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole
block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
- name: Read mkinitcpio configuration
when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.slurp:
src: /mnt/etc/mkinitcpio.conf
register: configuration_mkinitcpio_slurp
- name: Build mkinitcpio FILES list
when: _initramfs_generator | default('') == 'mkinitcpio'
vars:
mkinitcpio_files_list: >-
{{
(
configuration_mkinitcpio_slurp.content | b64decode
| regex_findall('^FILES=\\(([^)]*)\\)', multiline=True)
| default([])
| first
| default('')
).split()
}}
mkinitcpio_files_list_new: >-
{{
(
(mkinitcpio_files_list + [configuration_luks_keyfile_path])
if (configuration_luks_keyfile_in_use | default(false))
else (
mkinitcpio_files_list
| reject('equalto', configuration_luks_keyfile_path)
| list
)
)
| unique
}}
ansible.builtin.set_fact:
configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}"
- name: Configure mkinitcpio FILES list
when: _initramfs_generator | default('') == 'mkinitcpio'
ansible.builtin.lineinfile:
path: /mnt/etc/mkinitcpio.conf
regexp: "^FILES="
line: >-
FILES=({{
configuration_mkinitcpio_files_list_new | join(' ')
}})

View File

@@ -1,98 +0,0 @@
---
# 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,6 +86,7 @@
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,35 +1,26 @@
---
# 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/root
path: /mnt/tmp
prefix: luks-passphrase-
state: file
register: _tpm2_passphrase_tempfile
register: configuration_luks_tpm2_passphrase_tempfile
- name: Write passphrase into temporary file
- name: Write passphrase into temporary file for TPM2 enrollment
ansible.builtin.copy:
dest: "{{ _tpm2_passphrase_tempfile.path }}"
dest: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
content: "{{ configuration_luks_passphrase }}"
owner: root
group: root
mode: "0600"
no_log: true
- 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
- name: Enroll TPM2 token
vars:
_enroll_args: >-
configuration_luks_enroll_args: >-
{{
[
'/usr/bin/systemd-cryptenroll',
@@ -37,28 +28,70 @@
'--tpm2-with-pin=false',
'--wipe-slot=tpm2',
'--unlock-key-file=' + (
_tpm2_passphrase_tempfile.path | regex_replace('^/mnt', '')
configuration_luks_tpm2_passphrase_tempfile.path
| regex_replace('^/mnt', '')
)
]
+ (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs]
if configuration_luks_tpm2_pcrs | length > 0 else [])
+ [configuration_luks_device]
}}
ansible.builtin.command: "{{ chroot_command }} {{ _enroll_args | join(' ') }}"
register: _tpm2_enroll_result
changed_when: _tpm2_enroll_result.rc == 0
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
- 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: TPM2 enrollment failed
ansible.builtin.debug:
- name: Warn about TPM2 enrollment failure
ansible.builtin.fail:
msg: >-
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 }}
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
- name: Fallback to keyfile auto-decrypt
ansible.builtin.set_fact:
configuration_luks_auto_method: keyfile
always:
- name: Remove temporary passphrase file
when: _tpm2_passphrase_tempfile.path is defined
- name: Remove TPM2 enrollment passphrase file
when: configuration_luks_tpm2_passphrase_tempfile.path is defined
ansible.builtin.file:
path: "{{ _tpm2_passphrase_tempfile.path }}"
path: "{{ configuration_luks_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,8 +17,6 @@
- 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

@@ -1,8 +0,0 @@
---
- 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

@@ -1,115 +0,0 @@
---
- 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

@@ -1,45 +0,0 @@
---
- 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,12 +2,6 @@
- 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']
@@ -15,31 +9,12 @@
+ (['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.value.sudo is defined and (item.value.sudo | string | length > 0)
when: item.sudo | default(false)
vars:
configuration_sudoers_rule: >-
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
{{ item.sudo if item.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
ansible.builtin.copy:
content: "{{ item.key }} {{ configuration_sudoers_rule }}\n"
dest: "/mnt/etc/sudoers.d/{{ item.key }}"
content: "{{ item.name }} {{ configuration_sudoers_rule }}\n"
dest: "/mnt/etc/sudoers.d/{{ item.name }}"
mode: "0440"
validate: /usr/sbin/visudo --check --file=%s
loop: "{{ system_cfg.users | dict2items }}"
loop: "{{ system_cfg.users }}"
loop_control:
label: "{{ item.key }}"
label: "{{ item.name }}"

View File

@@ -1,6 +1,5 @@
---
- 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
@@ -10,13 +9,6 @@
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
@@ -26,43 +18,44 @@
- 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 + _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') }}
--uid {{ 1000 + ansible_loop.index0 }}
--groups {{ configuration_user_group }} {{ item.name }}
--password {{ item.password | password_hash('sha512') }} --shell {{ item.shell | default('/bin/bash') }}
ansible.builtin.command: "{{ configuration_useradd_cmd }}"
loop: "{{ system_cfg.users | dict2items }}"
loop: "{{ system_cfg.users }}"
loop_control:
index_var: _idx
label: "{{ item.key }}"
extended: true
label: "{{ item.name }}"
register: configuration_user_result
changed_when: configuration_user_result.rc == 0
no_log: true
- name: Ensure .ssh directory exists
when: (item.value['keys'] | default([]) | length) > 0
when: item['keys'] | default([]) | length > 0
ansible.builtin.file:
path: "/mnt/home/{{ item.key }}/.ssh"
path: "/mnt/home/{{ item.name }}/.ssh"
state: directory
owner: "{{ 1000 + _idx }}"
group: "{{ 1000 + _idx }}"
owner: "{{ 1000 + ansible_loop.index0 }}"
group: "{{ 1000 + ansible_loop.index0 }}"
mode: "0700"
loop: "{{ system_cfg.users | dict2items }}"
loop: "{{ system_cfg.users }}"
loop_control:
index_var: _idx
label: "{{ item.key }}"
extended: true
label: "{{ item.name }}"
- name: Deploy SSH authorized_keys
when: (item.value['keys'] | default([]) | 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 }}"
- 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 }}"
mode: "0600"
loop: "{{ system_cfg.users | dict2items }}"
create: true
loop: "{{ system_cfg.users | subelements('keys', skip_missing=True) }}"
loop_control:
index_var: _idx
label: "{{ item.key }}"
label: "{{ item.0.name }}: {{ item.1[:40] }}..."

View File

@@ -65,15 +65,3 @@ 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,10 +87,9 @@
- 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,23 +68,6 @@
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,7 +25,6 @@
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,21 +129,8 @@ 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,16 +144,27 @@
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 mapping
- system.users is not defined or (system.users is iterable and system.users is not string and system.users is not mapping)
- system.root is not defined or system.root is mapping
- system.luks is not defined or system.luks is mapping
- system.features is not defined or system.features is mapping
fail_msg: "system and its nested keys (network, root, luks, features, users) must be dictionaries."
fail_msg: "system and its nested keys (network, root, luks, features) must be dictionaries; system.users must be a list."
quiet: true
- name: Validate DNS lists (not strings)
@@ -25,17 +25,17 @@
quiet: true
- name: Validate system.users entries
when: system.users is defined and system.users is mapping and system.users | length > 0
when: system.users is defined and system.users | length > 0
ansible.builtin.assert:
that:
- item.value is mapping
- item.key | string | length > 0
- 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 keyed by username; 'keys' must be a list."
- item is mapping
- item.name is defined and (item.name | string | length) > 0
- item['keys'] is not defined or (item['keys'] is iterable and item['keys'] is not string)
fail_msg: "Each system.users[] entry must be a dict with 'name'; 'keys' must be a list."
quiet: true
loop: "{{ system.users | dict2items }}"
loop: "{{ system.users }}"
loop_control:
label: "{{ item.key }}"
label: "{{ item.name | default('(unnamed)') }}"
- name: Validate system features input types
when: system.features is defined

View File

@@ -81,14 +81,10 @@
when:
- system_cfg.type == "virtual"
- hypervisor_type != "vmware"
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact:
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_user: "{{ system_cfg.users[0].name }}"
ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
no_log: true
@@ -96,12 +92,12 @@
when: hypervisor_type == "vmware"
ansible.builtin.set_fact:
ansible_connection: vmware_tools
ansible_host: "{{ hypervisor_cfg.url }}"
ansible_port: 443
ansible_user: root
ansible_password: ""
ansible_vmware_host: "{{ hypervisor_cfg.url }}"
ansible_vmware_port: 443
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,27 +17,6 @@
- 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,16 +261,13 @@
fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size."
quiet: true
- name: Validate at least one user with a password is defined
vars:
_pw_users: "{{ system_cfg.users | dict2items | selectattr('value.password', 'defined') | list }}"
- name: Validate at least one user is defined
ansible.builtin.assert:
that:
- 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."
- 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[]."
quiet: true
no_log: true

View File

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

View File

@@ -9,13 +9,12 @@
- >-
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'] if os == 'archlinux' else []))
(system_cfg.filesystem == 'btrfs' and item.path in ['/home', '/var/log', '/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' or os != 'archlinux'))
not (item.path in ['/swap', '/var/cache/pacman/pkg'] and system_cfg.filesystem != 'btrfs')
- system_cfg.features.swap.enabled | bool or item.path != '/swap'
ansible.posix.mount:
path: /mnt{{ item.path }}

View File

@@ -21,6 +21,7 @@
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
@@ -50,6 +51,7 @@
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
@@ -77,6 +79,7 @@
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,7 +43,6 @@
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 }}
@@ -52,18 +51,12 @@
- { subvol: swap }
- { subvol: home }
- { subvol: var }
- { subvol: pkg, os: [archlinux] }
- { subvol: pkg }
- { subvol: var_log }
- { subvol: var_log_audit }
loop_control:
label: "{{ item.subvol }}"
- 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
register: partitioning_btrfs_subvol_result
- name: Set quotas for subvolumes
when: system_cfg.features.cis.enabled | bool
@@ -81,6 +74,7 @@
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')
and (
(system_cfg.luks.method | lower)
== 'tpm2'
)
or (system_cfg.features.secure_boot.enabled | default(false) | bool)
}}

View File

@@ -1,26 +0,0 @@
---
- 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
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
community.libvirt.virt:
name: "{{ hostname }}"
command: undefine
uri: "{{ libvirt_uri | default('qemu:///system') }}"
failed_when: false
- name: Remove libvirt disk images
@@ -70,7 +70,6 @@
- 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 | dict2items | selectattr('value.password', 'defined') | first).key }}"
cipassword: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first).value.password }}"
ciuser: "{{ system_cfg.users[0].name }}"
cipassword: "{{ system_cfg.users[0].password }}"
ciupgrade: false
vmid: "{{ system_cfg.id }}"
name: "{{ hostname }}"

View File

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

View File

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