Compare commits

...

171 Commits

Author SHA1 Message Date
MORAWSKI Norbert
2b17e147d7 fix(configuration): use /boot/grub2/grub.cfg for RedHat EFI grub config 2026-03-25 15:48:23 +01:00
c529e71ebc feat(packages): add needrestart to Debian and Ubuntu package lists 2026-03-20 18:06:14 +01:00
cb46de2b6d feat(bootstrap): add full package upgrade step for Debian and Ubuntu 2026-03-20 18:05:04 +01:00
9169117b25 fix(vim): use vimscript comment syntax for blockinfile markers in vimrc 2026-03-20 18:00:12 +01:00
6c94c519fb fix(sudo): use explicit string check instead of bool conditional for sudo field 2026-03-20 17:31:49 +01:00
efd96a42b8 fix(connection): set ansible_port explicitly at every connection transition 2026-03-20 17:31:49 +01:00
68661c3cca fix(vmware): use primary ansible_* vars for vmware_tools connection plugin precedence 2026-03-20 17:31:49 +01:00
1db20c7ac0 fix(vmware): use empty password for vmware_tools during live ISO bootstrap 2026-03-20 17:31:49 +01:00
7b155b427b fix(users): update cloud-init template and input validation for dict users 2026-03-20 17:31:49 +01:00
ca8721e98f refactor(prompts): remove vars_prompt, require users defined in inventory 2026-03-20 17:31:49 +01:00
cdb2559d8f fix(prompts): add default values to vars_prompt to skip in non-interactive mode 2026-03-20 17:31:49 +01:00
443f6623df refactor(users): change system.users from list to dict keyed by username 2026-03-20 17:31:49 +01:00
6cf418fe00 fix(configuration): make root password, user keys, and sudo all optional 2026-03-20 17:31:49 +01:00
47ec5fe621 fix(cloud-init): handle missing keys and make sudo conditional 2026-03-20 17:31:49 +01:00
240f945cce fix(cleanup): remove ansible_become override that blocks swapoff/umount 2026-03-20 17:31:49 +01:00
663a04556f feat(global_defaults): add system.features.aur schema for validation passthrough 2026-03-20 17:31:49 +01:00
6febd1acf1 refactor(virtualization): extract shared Xen disk definitions 2026-03-12 12:27:18 +01:00
008187860c refactor: remove unnecessary changed_when from set_fact tasks 2026-03-12 12:25:45 +01:00
cd1be6b5e1 refactor(partitioning): remove redundant blockdev --rereadpt calls 2026-03-12 12:25:15 +01:00
15be6149fd refactor(partitioning): remove unused register variables 2026-03-12 12:24:59 +01:00
ca29ad200d chore: suppress args[module] false positives from variable-based module_defaults 2026-03-12 12:12:27 +01:00
8079099cee fix(cleanup): add no_log to Proxmox VM restart task 2026-03-12 12:12:27 +01:00
9e79185b07 fix(virtualization): add missing changed_when to Xen VM stop task 2026-03-12 12:12:27 +01:00
b88bf2860f fix(configuration): replace fail+ignore_errors with debug for TPM2 fallback warning 2026-03-12 12:12:27 +01:00
81d26eb715 refactor(configuration): split encryption.yml into crypttab, dracut, grub, and initramfs subtasks 2026-03-12 09:40:40 +01:00
41691fcf0a feat(bootstrap): add rescue block with VM cleanup on failure 2026-03-12 07:43:51 +01:00
601f8a1ef9 feat(environment): VMware network config, DNS resolvers, and SSH switchover 2026-03-12 07:43:46 +01:00
49d362c860 fix(global_defaults): populate flat network fields from interfaces in pre-computed path 2026-03-12 07:43:39 +01:00
f9656cfbf5 feat(vmware): add VMware hypervisor support (node field, connection vars, validation) 2026-03-12 07:43:34 +01:00
c99daa3dbc fix(bootstrap): exclude tldr from Ubuntu rolling extra packages 2026-02-22 20:40:46 +01:00
d35976635c fix(global_defaults): use archive.ubuntu.com instead of mirror redirector 2026-02-22 16:26:35 +01:00
b13f89a250 fix(global_defaults): apply mirror default in pre-computed system_cfg path 2026-02-22 14:20:12 +01:00
b3b634f915 feat(configuration): add Debian/Ubuntu repository and apt configuration 2026-02-22 10:47:47 +01:00
b8dd400aea feat(bootstrap): use configurable mirror and write proper sources.list 2026-02-22 10:47:43 +01:00
f38e0a628f feat(global_defaults): add system.mirror to schema and normalization 2026-02-22 10:47:40 +01:00
3242d5a895 chore(bootstrap): update ubuntu non-lts codename to questing (25.10) 2026-02-22 03:08:54 +01:00
7e812dd74c fix(global_defaults): add missing ssh.enabled validation assertion 2026-02-22 03:08:31 +01:00
785eaab9a7 fix(global_defaults): correct fedora version upper bound to 43 2026-02-22 03:08:23 +01:00
81ff2b2b87 feat(global_defaults): add root.shell to system schema and normalization 2026-02-22 03:07:30 +01:00
2265e346b0 refactor(cleanup): remove duplicated libvirt path vars, reuse virtualization defaults 2026-02-22 03:07:04 +01:00
d9ae4ee809 refactor(bootstrap,configuration): rename validation-only _normalize.yml files 2026-02-22 03:06:34 +01:00
931d65df04 fix(partitioning): add | bool to all system_cfg.features.cis.enabled checks 2026-02-22 03:06:13 +01:00
59670e876a fix(partitioning): add partition separator for NVMe/mmcblk device paths 2026-02-22 02:39:36 +01:00
f7070343b9 refactor(configuration): centralize DNS list variables in network dispatch 2026-02-22 02:39:32 +01:00
1cce81366c refactor(configuration): extract shared BLS update task to reduce duplication 2026-02-22 02:39:28 +01:00
f6cb7bf78d fix(bootstrap): add missing --best flag to RHEL dnf commands 2026-02-22 02:39:23 +01:00
2c80c01b1a refactor(global_defaults): consolidate hypervisor auth into shared credential dicts 2026-02-22 02:35:04 +01:00
1b58a20c45 refactor(bootstrap,configuration,environment): add defaults/main.yml and extract hardcoded values 2026-02-22 02:32:36 +01:00
6b1686e652 refactor(bootstrap,configuration): add per-role _normalize.yml for platform resolution 2026-02-22 02:27:46 +01:00
a460584c5d refactor(configuration): add platform_config dict and replace is_rhel/is_debian with os_family lookups 2026-02-22 02:26:54 +01:00
9c0f00f1ec feat(global_defaults): add os_family_map and os_family fact for platform config lookups 2026-02-22 02:23:05 +01:00
6ebceb8ee2 fix(virtualization): add vTPM2 result validation before VMware power-on 2026-02-22 02:22:37 +01:00
5e72394bf8 feat(global_defaults): add semantic validations for IP, hostname, LUKS method, and interface prefix 2026-02-22 02:22:05 +01:00
5abdc76c86 refactor(global_defaults): extract physical_default_os to configurable default 2026-02-22 02:21:34 +01:00
bcfd5d5a89 fix(global_defaults): normalize system.type 'vm' to 'virtual' for main project compatibility 2026-02-22 02:21:22 +01:00
c91e049378 docs(bootstrap): add section comments, role boundary docs, and pipeline overview 2026-02-22 01:59:12 +01:00
b9e8aa283b refactor(global_defaults): data-driven hypervisor validation and shared constants 2026-02-22 01:59:09 +01:00
734ed822d6 refactor(extras): convert custom.sh from template to static copy 2026-02-22 01:59:04 +01:00
3f2f4055f0 fix(cleanup,config): xen tmp cleanup, tpm2 fallback warning, add code comments 2026-02-22 01:59:01 +01:00
a2b206127f fix(partitioning,network): swapon idempotency, DNS search domains, tune2fs changed_when 2026-02-22 01:58:56 +01:00
6985235e70 fix(encryption): add no_log to LUKS configuration block 2026-02-22 01:58:52 +01:00
25b1eeec45 fix(network): bind NM connections to detected interface names for multi-NIC 2026-02-21 16:51:15 +01:00
3f65585e5c fix(bootstrap): make dhcp-client conditional for EL < 10 (removed in EL 10) 2026-02-21 13:43:41 +01:00
74f1365a06 fix(bootstrap): remove --asexplicit from pacstrap to preserve dependency metadata 2026-02-21 13:26:59 +01:00
9d19f628aa fix(bootstrap): add kernel package to rocky and almalinux extra packages 2026-02-21 12:06:09 +01:00
ced0da7bd1 fix(bootstrap): detect kernel package name for dnf family reinstall step 2026-02-21 11:46:57 +01:00
cf49d30916 fix(bootstrap): ensure chroot DNS resolution before installing extra packages 2026-02-21 11:30:28 +01:00
46b5223da5 fix(environment): align repo IDs in rocky and almalinux templates with bootstrap config 2026-02-21 11:18:34 +01:00
494f0b58b2 fix(configuration): omit interface-name when not explicitly provided to avoid predictable naming mismatch 2026-02-21 08:29:24 +01:00
d84b867cef refactor(configuration): rename _uid to configuration_uid for role prefix convention 2026-02-21 05:14:33 +01:00
39c786305f fix(configuration): handle boolean sudo values in sudoers deployment 2026-02-21 05:14:29 +01:00
72e2263f5c fix(configuration): use full path for chpasswd in chroot 2026-02-21 05:03:36 +01:00
ac532578b8 fix(global_defaults): enrich pre-computed system_cfg with bootstrap defaults 2026-02-21 04:24:23 +01:00
34f35bb5ac chore(lint): suppress var-naming for user-facing API dicts 2026-02-21 02:58:10 +01:00
6de88a911a fix(configuration): remove unnecessary changed_when on set_fact tasks 2026-02-21 02:56:58 +01:00
fa78edf2e2 refactor(cis): align normalization with main project activation gate pattern 2026-02-21 02:56:39 +01:00
a1c8b5e2dd fix(global_defaults): remove dead /swap and make pacman cache arch-only in reserved mounts 2026-02-21 02:56:20 +01:00
19da8c0e68 fix(global_defaults): set filesystem default to ext4 instead of empty string 2026-02-21 02:56:08 +01:00
ff1a4df960 refactor(bootstrap): restructure package lists to self-contained per-OS dicts with base/extra/conditional 2026-02-21 02:39:06 +01:00
f0c0b54e7f refactor(environment): split main.yml into focused sub-task files 2026-02-21 02:39:05 +01:00
a868c6bb47 refactor(global_defaults): add idempotency guards to normalization tasks 2026-02-21 02:39:03 +01:00
dd0d70f4fd fix(global_defaults): default interface name to eth0 instead of empty string 2026-02-21 02:38:59 +01:00
c08e1fe4e0 docs(cis): add comment explaining squashfs/snap Ubuntu exclusion 2026-02-21 02:38:58 +01:00
c3ccce97ae chore(bootstrap): pin collection versions in requirements.yml 2026-02-21 02:38:57 +01:00
d9ca905b73 fix(bootstrap): move Jinja to end of task name and rename registers to bootstrap_dnf_* 2026-02-21 02:38:27 +01:00
6085336f96 docs: update README with cis dict API, execution pipeline, and cleanup defaults 2026-02-21 01:30:36 +01:00
2831479e77 fix(validation): align btrfs disk size check with new 2GB swap minimum 2026-02-21 01:28:32 +01:00
608cbf3196 refactor(bootstrap): unify rocky, almalinux, and fedora into shared _dnf_family.yml 2026-02-21 01:27:33 +01:00
382e48176d refactor(cis): extract hardcoded values to cis_defaults and add _normalize.yml 2026-02-21 01:26:31 +01:00
0372e35ea3 refactor(cleanup): prioritize source-match over target-match in libvirt media removal 2026-02-21 01:22:44 +01:00
6e055de457 docs(cis): explain Fedora exclusion from crypto-policy configuration 2026-02-21 01:22:41 +01:00
f7e1bd4d49 fix(bootstrap): replace brittle sed with ansible.builtin.replace for ubuntu universe repo 2026-02-21 01:22:37 +01:00
58c9b264f9 refactor(virtualization): simplify cloud-user-data sudo to unconditional NOPASSWD 2026-02-21 01:22:34 +01:00
11a4794ac2 fix(bootstrap): remove duplicate lrzsz and gate dbus-daemon on version in almalinux 2026-02-21 01:20:34 +01:00
d3c8c6c975 fix(virtualization): fix cloud-user-data sudo logic to respect sudo: false 2026-02-21 01:20:31 +01:00
ba8ab340f7 fix(partitioning): lower swap minimum from 4GB to 2GB for small VMs 2026-02-21 01:19:23 +01:00
474ebbb513 fix(partitioning): add wipefs before mkfs on extra disk partitions 2026-02-21 01:19:19 +01:00
5df369b151 fix(cis): strengthen kernel module blacklist and sysctl hardening 2026-02-21 01:18:52 +01:00
08c518bd5b refactor(partitioning): split monolithic main.yml into focused task files 2026-02-21 00:39:03 +01:00
e200774c8e fix(validation): add CIDR prefix range check and Ubuntu version validation 2026-02-21 00:38:57 +01:00
6e0c289226 refactor(cis): remove redundant AllowUsers/AllowGroups/DenyUsers/DenyGroups from sshd 2026-02-21 00:38:52 +01:00
3be725633e fix(cis): skip squashfs blacklist on Ubuntu to preserve snap functionality 2026-02-21 00:38:47 +01:00
6c02eab159 fix(partitioning): correct changed_when on btrfs quota and qgroup commands 2026-02-21 00:38:43 +01:00
99c579bec0 fix(cis): add regexp to all lineinfile entries in security_lines.yml for idempotency 2026-02-21 00:38:36 +01:00
be5d2e9f94 fix: add no_log to credential-handling pre_tasks and post_tasks in main.yml 2026-02-21 00:38:32 +01:00
e334c82b26 fix(virtualization): add no_log and secure temp file handling to libvirt cloud-init 2026-02-21 00:38:28 +01:00
5008d97bc8 refactor(cleanup): add configurable verify_boot, boot_timeout, and remove_on_failure defaults 2026-02-20 23:02:24 +01:00
06b8058c1d refactor: move playbook-root templates into their respective roles 2026-02-20 23:01:38 +01:00
aec82e4241 refactor: add loop_control labels to dict-based loops across all roles 2026-02-20 23:00:53 +01:00
f36d9b7ca3 refactor(partitioning): move btrfs home quota to configurable default 2026-02-20 22:55:37 +01:00
0950db7011 fix(environment): detect RHEL ISO device dynamically instead of hardcoded /dev/sr paths 2026-02-20 22:54:42 +01:00
4f3e39398f refactor(global_defaults): split system.yml into composable normalization stages 2026-02-20 22:54:05 +01:00
e3c21168fd refactor(global_defaults): extract OS family lists to single source of truth 2026-02-20 22:52:55 +01:00
643fec1cc6 fix(partitioning): add failed_when to all blkid commands to catch empty UUIDs 2026-02-20 22:52:18 +01:00
bbbdcfc9b6 fix(partitioning): add default fallbacks for is_rhel, os, os_version in defaults 2026-02-20 22:51:37 +01:00
9347140808 fix(virtualization): use hostname variable instead of hardcoded archiso in cloud-user-data 2026-02-20 22:51:32 +01:00
b8af8b3fdd fix(virtualization): avoid no-handler lint finding in xen VM created tracking 2026-02-20 22:29:03 +01:00
94ea082e63 fix(partitioning): fix line length violation in home size calculation 2026-02-20 22:28:58 +01:00
3361ee3de8 fix(configuration): add pipefail to root password shell pipe 2026-02-20 22:28:54 +01:00
06f6203674 fix(bootstrap): use release map for ubuntu version detection 2026-02-20 22:27:46 +01:00
a385c27963 chore: add .yamllint matching main project conventions 2026-02-20 22:27:31 +01:00
04340d1a04 fix(configuration): use chpasswd for root password and separate shell setting 2026-02-20 22:27:17 +01:00
4c8021fc2e fix(configuration): add explicit LUKS auto-decrypt fallback state tracking and logging 2026-02-20 22:26:47 +01:00
6a6a43ae96 refactor(partitioning): externalize hardcoded LVM and disk sizing constants to defaults 2026-02-20 22:26:23 +01:00
2a7340af37 fix(virtualization): add xen VM existence check and improve changed_when 2026-02-20 22:25:10 +01:00
e0687269d4 fix(cis): add pipefail to sshd version detection and define binary defaults 2026-02-20 22:24:14 +01:00
1634af552e feat(cleanup): gate RHEL ISO disk and fstab handling on rhel_repo.source 2026-02-20 21:51:20 +01:00
0077f05654 feat(global_defaults): add system.features.rhel_repo option (iso|satellite|none) 2026-02-20 21:51:16 +01:00
33d46274bd fix(encryption): add warning before silent TPM2-to-keyfile fallback 2026-02-20 21:51:12 +01:00
ed6b604302 fix(partitioning): correct wipefs changed_when to report actual disk modification 2026-02-20 21:51:09 +01:00
fc2ddfea8a fix(validation): require password for primary user in system.users[0] 2026-02-20 21:51:06 +01:00
efdbc0c04e fix(system_check): move no_log from block to individual API tasks 2026-02-20 21:51:02 +01:00
5769bd456d fix(cis): make mlkem768x25519-sha256 KexAlgorithm conditional on OpenSSH 9.9+ 2026-02-20 21:50:58 +01:00
b7ffcfecd4 fix(cis): use is_rhel for journald config path instead of fedora-only check 2026-02-20 21:50:55 +01:00
f18881328c refactor(configuration): add conditional dispatch to task includes 2026-02-20 21:16:52 +01:00
05aeb0676b refactor(cis): move OS-specific binary resolution to vars/main.yml 2026-02-20 21:16:48 +01:00
5b5c94cb8b refactor(configuration): split network.yml into per-init-system dispatch files 2026-02-20 21:16:45 +01:00
4a89911a54 refactor(bootstrap): restructure conditional package lists to list concatenation 2026-02-20 21:16:40 +01:00
b61fecfc88 refactor(configuration): convert services.yml to list-based loop 2026-02-20 21:16:37 +01:00
b690bddaec refactor(virt): adopt module_defaults for hypervisor credentials 2026-02-20 21:16:33 +01:00
8e92f40b2a refactor(cleanup): restructure dispatch to use hypervisor_type include 2026-02-20 21:16:28 +01:00
c8c9a9c9f5 refactor(partitioning): extract VG name to defaults variable 2026-02-20 21:16:25 +01:00
7a666239b6 fix(configuration): remove trailing blank line from extras.yml 2026-02-20 20:20:33 +01:00
7181679d7c docs(environment): document RPM GPG policy relaxation 2026-02-20 20:19:57 +01:00
32f22e94bd chore(bootstrap): align ansible.cfg with main project settings 2026-02-20 20:19:46 +01:00
15122b924d feat(system_check): add safety check for physical installs 2026-02-20 20:19:37 +01:00
be51bfe101 fix(cleanup): fix vmware CD-ROM omit fragility and add cross-role defaults 2026-02-20 20:19:25 +01:00
83610447e7 fix(virtualization): add XML safety attributes and switch xen to virtio 2026-02-20 20:18:49 +01:00
1fc64b9e5d fix(cis): remove deprecated sshd options and update hardening values 2026-02-20 20:17:52 +01:00
bbf83f7050 fix(configuration): disambiguate BLS task names and clean up misc noise 2026-02-20 20:17:05 +01:00
2a044dcc1d refactor(configuration): relocate login banner and fix blockinfile markers 2026-02-20 20:16:19 +01:00
c57323ff69 fix(configuration): use short hostname and allow per-user shell 2026-02-20 20:15:49 +01:00
b8c3b49419 fix(partitioning): mount extra disks by UUID instead of device path 2026-02-20 20:15:25 +01:00
80e7e2cdd6 fix(partitioning): correct LVM swap sizing and harden UUID fallbacks 2026-02-20 20:15:00 +01:00
ab9502ea49 fix(configuration): add trailing semicolons to NM keyfile DNS fields 2026-02-20 20:14:06 +01:00
b0c7a39749 fix(bootstrap): add missing packages and remove duplicates 2026-02-20 20:13:53 +01:00
64b1296fe2 fix(bootstrap): add devpts mount and use ephemeral state for RHEL DVD 2026-02-20 20:12:59 +01:00
bbe3ad9a07 fix(bootstrap): unify resolv.conf to live environment DNS symlink 2026-02-20 20:12:42 +01:00
e2241bb223 fix(global_defaults): add no_log to hypervisor tasks and expand validation 2026-02-20 20:11:37 +01:00
6236978e45 fix: configurable OVMF/machine type, routes syntax, package lists, interface names 2026-02-20 18:47:12 +01:00
ebc5db1c59 fix(cleanup): keep RHEL ISO ide1 attached as local repo 2026-02-20 18:41:40 +01:00
4d0bf3891a fix: deep analysis audit — no_log, resolv.conf, service conflicts, lint 2026-02-20 18:34:59 +01:00
14ff79cfd0 fix(bootstrap): RHEL 9 bootstrap from Arch ISO compatibility 2026-02-20 16:58:59 +01:00
8070cc4196 refactor: make bootstrap host target configurable 2026-02-20 16:58:59 +01:00
6e53af5e92 fix(ubuntu): add initramfs-tools to debootstrap base packages 2026-02-20 16:58:59 +01:00
6d84a21130 fix(bootstrap): use explicit keyring for debootstrap and copy resolv.conf 2026-02-20 16:58:59 +01:00
b3132329cb fix(cloud-init): handle boolean sudo values in user-data template 2026-02-20 16:58:59 +01:00
a85308185f fix: re-gather facts after reboot to detect target OS package manager 2026-02-20 16:58:59 +01:00
d1d579c658 fix: resolve Jinja2 .keys ambiguity, fastfetch availability, and python interpreter 2026-02-20 16:58:58 +01:00
e08532ffd0 fix(partitioning): create separate /boot for LVM-based filesystems 2026-02-20 04:50:32 +01:00
2a543fffc3 fix(bootloader): run efibootmgr on host for universal chroot compatibility 2026-02-20 03:36:20 +01:00
117 changed files with 4376 additions and 3136 deletions

View File

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

19
.yamllint Normal file
View File

@@ -0,0 +1,19 @@
---
extends: default
rules:
document-start: disable
line-length:
max: 200
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: true
truthy:
allowed-values: ["true", "false"]
check-keys: false
comments:
min-spaces-from-content: 1
comments-indentation: disable
braces:
max-spaces-inside: 1
octal-values:
forbid-implicit-octal: true

502
README.md
View File

@@ -1,8 +1,8 @@
# Ansible Bootstrap
An Ansible playbook for automating Linux system bootstrap in an Infrastructure-as-Code manner. It uses the Arch Linux ISO as a foundational tool to provide an efficient and systematic method for the automatic deployment of a variety of Linux distributions on designated target systems, ensuring a standardized setup across different platforms.
Automated Linux system bootstrap using the Arch Linux ISO as a universal installer. Deploys any supported distribution on virtual or physical targets via Infrastructure-as-Code.
Most roles are adaptable for use with systems beyond Arch Linux, requiring only that the target system can install the necessary package manager (e.g. `dnf` for RHEL-based systems). A replacement for the `arch-chroot` command may also be required; set `system.features.chroot.tool` accordingly.
Non-Arch targets require the appropriate package manager available from the ISO environment (e.g. `dnf` for RHEL-family). Set `system.features.chroot.tool` if `arch-chroot` is unavailable.
## Table of Contents
@@ -13,34 +13,33 @@ Most roles are adaptable for use with systems beyond Arch Linux, requiring only
- 4.1 [Core Variables](#41-core-variables)
- 4.2 [`system` Dictionary](#42-system-dictionary)
- 4.3 [`hypervisor` Dictionary](#43-hypervisor-dictionary)
- 4.4 [VMware Guest Operations](#44-vmware-guest-operations)
- 4.5 [Multi-Disk Schema](#45-multi-disk-schema)
- 4.6 [Advanced Partitioning Overrides](#46-advanced-partitioning-overrides)
5. [How to Use the Playbook](#5-how-to-use-the-playbook)
- 5.1 [Prerequisites](#51-prerequisites)
- 5.2 [Running the Playbook](#52-running-the-playbook)
- 5.3 [Example Usage](#53-example-usage)
6. [Security](#6-security)
7. [Operational Notes](#7-operational-notes)
- 4.4 [`cis` Dictionary](#44-cis-dictionary)
- 4.5 [VMware Guest Operations](#45-vmware-guest-operations)
- 4.6 [Multi-Disk Schema](#46-multi-disk-schema)
- 4.7 [Advanced Partitioning Overrides](#47-advanced-partitioning-overrides)
- 4.8 [Cleanup Defaults](#48-cleanup-defaults)
5. [Execution Pipeline](#5-execution-pipeline)
6. [Usage](#6-usage)
7. [Security](#7-security)
8. [Safety](#8-safety)
## 1. Supported Platforms
### Distributions
| `system.os` | Distribution | `system.version` |
| ------------ | ------------------------ | ------------------------------- |
| `almalinux` | AlmaLinux | `8`, `9`, `10` |
| `alpine` | Alpine Linux | latest (rolling) |
| `archlinux` | Arch Linux | latest (rolling) |
| `debian` | Debian | `10`, `11`, `12`, `13`, `unstable` |
| `fedora` | Fedora | `40`, `41`, `42`, `43` |
| `opensuse` | openSUSE Tumbleweed | latest (rolling) |
| `rhel` | Red Hat Enterprise Linux | `8`, `9`, `10` |
| `rocky` | Rocky Linux | `8`, `9`, `10` |
| `ubuntu` | Ubuntu | latest |
| `ubuntu-lts` | Ubuntu LTS | latest |
| `void` | Void Linux | latest (rolling) |
| `system.os` | Distribution | `system.version` |
| ------------ | ------------------------ | ------------------------------------- |
| `almalinux` | AlmaLinux | `8`, `9`, `10` |
| `alpine` | Alpine Linux | latest (rolling) |
| `archlinux` | Arch Linux | latest (rolling) |
| `debian` | Debian | `10`-`13`, `unstable` |
| `fedora` | Fedora | `38`-`45` |
| `opensuse` | openSUSE Tumbleweed | latest (rolling) |
| `rhel` | Red Hat Enterprise Linux | `8`, `9`, `10` |
| `rocky` | Rocky Linux | `8`, `9`, `10` |
| `ubuntu` | Ubuntu (latest non-LTS) | optional (e.g. `24.04`) |
| `ubuntu-lts` | Ubuntu LTS | optional (e.g. `24.04`) |
| `void` | Void Linux | latest (rolling) |
### Hypervisors
@@ -55,28 +54,28 @@ Most roles are adaptable for use with systems beyond Arch Linux, requiring only
## 2. Compatibility Notes
- `rhel_iso` is required for `system.os: rhel`.
- RHEL installs should use `system.filesystem: ext4` or `system.filesystem: xfs` (not `btrfs`).
- For RHEL 8 specifically, prefer `ext4` over `xfs` if you hit installer/filesystem edge cases.
- `custom_iso: true` skips ArchISO validation and pacman preparation; your installer image must already provide required tooling.
- On non-Arch installers, set `system.features.chroot.tool` (`arch-chroot`, `chroot`, or `systemd-nspawn`) explicitly as needed.
- RHEL installs should use `ext4` or `xfs` (not `btrfs`).
- `custom_iso: true` skips ArchISO validation; your installer must provide required tooling.
- On non-Arch installers, set `system.features.chroot.tool` explicitly.
## 3. Configuration Model
The project uses two dict-based variables:
Two dict-based variables drive the entire configuration:
- `system` for host/runtime/install configuration
- `hypervisor` for virtualization backend configuration
- **`system`** -- host, network, users, disk layout, encryption, and feature toggles
- **`hypervisor`** -- virtualization backend credentials and targeting
These are normal Ansible variables and belong in host/group vars. You can define them in inventory host entries, `group_vars/*`, or `host_vars/*`. Dictionary variables are merged across scopes (`group_vars` -> `host_vars`) by project config (`hash_behaviour = merge`), so you can set shared values like `system.filesystem` once in group vars and override only host-specific keys per host.
An optional third dict **`cis`** overrides CIS hardening parameters when `system.features.cis.enabled: true`.
All three are standard Ansible variables. Place them in `group_vars/`, `host_vars/`, or inline inventory. With `hash_behaviour = merge`, dictionaries merge across scopes, so shared values go in group vars and host-specific overrides go per-host.
### Variable Placement
| Location | Scope | Typical use |
| -------------------------- | ----------- | ------------------------------------------------------------ |
| `group_vars/all.yml` | All hosts | Shared defaults like `hypervisor`, `system.filesystem`, `boot_iso` |
| `group_vars/<group>.yml` | Group | Environment or role-specific defaults |
| `host_vars/<host>.yml` | Single host | Host-specific overrides |
| Inventory inline host vars | Single host | Inline definitions for quick setup |
| Location | Scope | Typical use |
| ------------------------ | ----------- | -------------------------------------------------------------- |
| `group_vars/all.yml` | All hosts | Shared `hypervisor`, `system.filesystem`, `boot_iso` |
| `group_vars/<group>.yml` | Group | Environment-specific defaults |
| `host_vars/<host>.yml` | Single host | Host-specific overrides (`system.network.ip`, `system.id`, etc.) |
### Example Inventory
@@ -90,8 +89,9 @@ all:
type: proxmox
url: pve01.example.com
username: root@pam
password: CHANGE_ME
host: pve01
password: !vault |
$ANSIBLE_VAULT...
node: pve01
storage: local-lvm
children:
@@ -107,7 +107,6 @@ all:
id: 101
cpus: 2
memory: 4096
balloon: 0
network:
bridge: vmbr0
ip: 10.0.0.10
@@ -124,19 +123,24 @@ all:
fstype: xfs
users:
- name: ops
password: CHANGE_ME
password: !vault |
$ANSIBLE_VAULT...
keys:
- "ssh-ed25519 AAAA..."
sudo: true
root:
password: CHANGE_ME
password: !vault |
$ANSIBLE_VAULT...
luks:
enabled: true
passphrase: CHANGE_ME
auto: true
passphrase: !vault |
$ANSIBLE_VAULT...
method: tpm2
tpm2:
pcrs: "7"
features:
cis:
enabled: true
firewall:
enabled: true
backend: firewalld
@@ -147,270 +151,280 @@ all:
### 4.1 Core Variables
These top-level variables sit outside the `system`/`hypervisor` dictionaries.
Top-level variables outside `system`/`hypervisor`/`cis`.
| Variable | Type | Description |
| ----------------------------------- | ------ | ------------------------------------------------ |
| `boot_iso` | string | Path to the boot ISO image (required for virtual installs). |
| `rhel_iso` | string | Path to the RHEL ISO (required when `system.os: rhel`). |
| `custom_iso` | bool | Skip ArchISO validation and pacman setup. Default `false`. |
| `thirdparty_tasks` | string | Drop-in task file included during environment setup. Default `dropins/preparation.yml`. |
| Variable | Type | Default | Description |
| ---------------- | ------ | -------------------------- | ---------------------------------------------------- |
| `boot_iso` | string | -- | Boot ISO path (required for virtual installs) |
| `rhel_iso` | string | -- | RHEL ISO path (required when `system.os: rhel`) |
| `custom_iso` | bool | `false` | Skip ArchISO validation and pacman setup |
| `thirdparty_tasks` | string | `dropins/preparation.yml` | Drop-in task file included during environment setup |
### 4.2 `system` Dictionary
Top-level host install/runtime settings. Use these keys under `system`.
| Key | Type | Default | Description |
| ------------ | ---------- | -------------------- | ---------------------------------------- |
| `type` | string | `virtual` | `virtual` or `physical` |
| `os` | string | empty | Target distribution (see [table](#distributions)) |
| `version` | string | empty | Version selector for distro families |
| `filesystem` | string | empty | `btrfs`, `ext4`, or `xfs` |
| `name` | string | inventory hostname | Final hostname |
| `timezone` | string | `Europe/Vienna` | System timezone (tz database name) |
| `locale` | string | `en_US.UTF-8` | System locale |
| `keymap` | string | `us` | Console keymap (`vconsole.conf`) |
| `id` | int/string | empty | VMID (required for Proxmox) |
| `cpus` | int | `0` | vCPU count |
| `memory` | int | `0` | Memory in MiB |
| `balloon` | int | `0` | Balloon memory in MiB |
| `path` | string | empty | Hypervisor folder/path (libvirt/vmware) |
| `packages` | list | `[]` | Additional packages installed post-reboot |
| `network` | dict | see below | Network configuration |
| `disks` | list | `[]` | Disk layout (see [Multi-Disk Schema](#45-multi-disk-schema)) |
| `users` | list | `[]` | User accounts (see below) |
| `root` | dict | see below | Root account settings |
| `luks` | dict | see below | Encryption settings |
| `features` | dict | see below | Feature toggles |
| Key | Type | Default | Description |
| ------------ | ---------- | ------------------ | ------------------------------------------------------ |
| `type` | string | `virtual` | `virtual` or `physical` |
| `os` | string | -- | Target distribution (see [table](#distributions)) |
| `version` | string | -- | Version selector for versioned distros |
| `filesystem` | string | -- | `btrfs`, `ext4`, or `xfs` |
| `name` | string | inventory hostname | Final hostname |
| `timezone` | string | `Europe/Vienna` | System timezone (tz database name) |
| `locale` | string | `en_US.UTF-8` | System locale |
| `keymap` | string | `us` | Console keymap |
| `id` | int/string | -- | VMID (required for Proxmox) |
| `cpus` | int | `0` | vCPU count (required for virtual) |
| `memory` | int | `0` | Memory in MiB (required for virtual) |
| `balloon` | int | `0` | Balloon memory in MiB (Proxmox) |
| `path` | string | -- | Hypervisor folder/path |
| `packages` | list | `[]` | Additional packages installed post-reboot |
| `network` | dict | see below | Network configuration |
| `disks` | list | `[]` | Disk layout (see [Multi-Disk Schema](#46-multi-disk-schema)) |
| `users` | list | `[]` | User accounts |
| `root` | dict | see below | Root account settings |
| `luks` | dict | see below | Encryption settings |
| `features` | dict | see below | Feature toggles |
#### `system.network`
| Key | Type | Default | Description |
| -------------- | ---------- | ------- | ---------------------------------------------------- |
| `bridge` | string | empty | Hypervisor network/bridge name |
| `vlan` | string/int | empty | VLAN tag |
| `ip` | string | empty | Static IP (omit for DHCP) |
| `prefix` | int | empty | CIDR prefix for static IP |
| `gateway` | string | empty | Default gateway (static only) |
| `dns.servers` | list | `[]` | DNS resolvers (must be a YAML list) |
| `dns.search` | list | `[]` | Search domains (must be a YAML list) |
| `interfaces` | list | `[]` | Multi-NIC config (overrides flat fields above) |
| Key | Type | Default | Description |
| -------------- | ---------- | ------- | ---------------------------------------------- |
| `bridge` | string | -- | Hypervisor network/bridge name |
| `vlan` | string/int | -- | VLAN tag |
| `ip` | string | -- | Static IP (omit for DHCP) |
| `prefix` | int | -- | CIDR prefix (1-32, required with `ip`) |
| `gateway` | string | -- | Default gateway |
| `dns.servers` | list | `[]` | DNS resolvers (must be a YAML list) |
| `dns.search` | list | `[]` | Search domains (must be a YAML list) |
| `interfaces` | list | `[]` | Multi-NIC config (overrides flat fields above) |
When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`, `vlan`) are auto-wrapped into a single-entry `interfaces[]` list. When `interfaces` is set, it takes precedence and the flat fields are back-populated from `interfaces[0]` for backward compatibility. Each `interfaces[]` entry supports: `name`, `bridge` (required), `vlan`, `ip`, `prefix`, `gateway`.
When `interfaces` is empty, the flat fields (`bridge`, `ip`, `prefix`, `gateway`, `vlan`) are auto-wrapped into a single-entry list. When `interfaces` is set, it takes precedence. Each entry supports: `name`, `bridge` (required), `vlan`, `ip`, `prefix`, `gateway`.
#### `system.users`
A list of user account dictionaries. Credentials for the first user are prompted interactively by default via `vars_prompt` in `main.yml`, but can be supplied via inventory, vars files, or `-e` for non-interactive runs.
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).
| Key | Type | Default | Description |
| ---------- | ------ | ------- | -------------------------------------------- |
| `name` | string | empty | Username created on target (required) |
| `password` | string | empty | User password (also used for sudo) |
| `keys` | list | `[]` | SSH public keys for `authorized_keys` |
| `sudo` | string | empty | Custom sudoers rule (optional, per-user) |
```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) |
| `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`.
#### `system.root`
| Key | Type | Default | Description |
| ---------- | ------ | ------- | -------------- |
| `password` | string | empty | Root password |
| Key | Type | Default | Description |
| ---------- | ------ | ------- | ------------- |
| `password` | string | -- | Root password |
#### `system.luks`
LUKS container, unlock, and initramfs-related settings.
| Key | Type | Default | Allowed | Description |
| ------------ | ------ | ------------------ | -------------------------- | ------------------------------------------ |
| `enabled` | bool | `false` | `true`/`false` | Enable encrypted root workflow |
| `passphrase` | string | empty | any string | Passphrase used for format/open/enroll |
| `mapper` | string | `SYSTEM_DECRYPTED` | mapper name | Mapper name under `/dev/mapper` |
| `auto` | bool | `true` | `true`/`false` | Auto-unlock behavior toggle |
| `method` | string | `tpm2` | `tpm2`, `keyfile` | Auto-unlock backend when `auto=true` |
| `keysize` | int | `64` | positive int | Keyfile size (bytes) for keyfile mode |
| `options` | string | `discard,tries=3` | crypttab opts | Additional crypttab/kernel options |
| `type` | string | `luks2` | cryptsetup type | LUKS format type |
| `cipher` | string | `aes-xts-plain64` | cipher name | Cryptsetup cipher |
| `hash` | string | `sha512` | hash name | Cryptsetup hash |
| `iter` | int | `4000` | positive int | PBKDF iteration time (ms) |
| `bits` | int | `512` | positive int | Key size (bits) |
| `pbkdf` | string | `argon2id` | pbkdf name | PBKDF algorithm |
| `urandom` | bool | `true` | `true`/`false` | Use urandom during key generation |
| `verify` | bool | `true` | `true`/`false` | Verify passphrase during format |
| Key | Type | Default | Description |
| ------------ | ------ | ------------------ | ------------------------------------------ |
| `enabled` | bool | `false` | Enable encrypted root |
| `passphrase` | string | -- | Passphrase for format/open/enroll |
| `mapper` | string | `SYSTEM_DECRYPTED` | Mapper name under `/dev/mapper` |
| `auto` | bool | `true` | Auto-unlock toggle |
| `method` | string | `tpm2` | Auto-unlock backend: `tpm2` or `keyfile` |
| `keysize` | int | `64` | Keyfile size in bytes |
| `options` | string | `discard,tries=3` | Additional crypttab options |
| `type` | string | `luks2` | LUKS format type |
| `cipher` | string | `aes-xts-plain64` | Cipher |
| `hash` | string | `sha512` | Hash algorithm |
| `iter` | int | `4000` | PBKDF iteration time (ms) |
| `bits` | int | `512` | Key size (bits) |
| `pbkdf` | string | `argon2id` | PBKDF algorithm |
| `urandom` | bool | `true` | Use urandom during key generation |
| `verify` | bool | `true` | Verify passphrase during format |
#### `system.luks.tpm2`
TPM2-specific policy settings used when `system.luks.method: tpm2`.
| Key | Type | Default | Allowed | Description |
| ------ | ----------- | ------- | --------------------- | --------------------------------------------------- |
| `device` | string | `auto` | `auto` or device path | TPM2 device selector |
| `pcrs` | string/list | empty | PCR expression | PCR binding policy (e.g. `"7"` or `"0+7"`) |
| Key | Type | Default | Description |
| -------- | ------------- | ------- | ---------------------------------------------- |
| `device` | string | `auto` | TPM2 device selector |
| `pcrs` | string/list | -- | PCR binding policy (e.g. `"7"` or `"0+7"`) |
#### `system.features`
Feature toggles for optional system configuration.
| Key | Type | Default | Allowed | Description |
| ------------------ | ------ | -------------- | ------------------------------------------ | ---------------------------------- |
| `cis.enabled` | bool | `false` | `true`/`false` | Enable CIS hardening role |
| `selinux.enabled` | bool | `true` | `true`/`false` | SELinux management |
| `firewall.enabled` | bool | `true` | `true`/`false` | Enable firewall role actions |
| `firewall.backend` | string | `firewalld` | `firewalld`, `ufw` | Firewall service backend |
| `firewall.toolkit` | string | `nftables` | `nftables`, `iptables` | Packet filtering toolkit |
| `ssh.enabled` | bool | `true` | `true`/`false` | SSH service/package management |
| `zstd.enabled` | bool | `true` | `true`/`false` | zstd related tuning |
| `swap.enabled` | bool | `true` | `true`/`false` | Swap setup toggle |
| `banner.motd` | bool | `false` | `true`/`false` | MOTD banner management |
| `banner.sudo` | bool | `true` | `true`/`false` | Sudo banner management |
| `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, `systemd-nspawn` | Chroot wrapper command |
| Key | Type | Default | Description |
| ------------------ | ------ | -------------- | ------------------------------------ |
| `cis.enabled` | bool | `false` | Enable CIS hardening (see [4.4](#44-cis-dictionary)) |
| `selinux.enabled` | bool | `true` | SELinux management |
| `firewall.enabled` | bool | `true` | Firewall setup |
| `firewall.backend` | string | `firewalld` | `firewalld` or `ufw` |
| `firewall.toolkit` | string | `nftables` | `nftables` or `iptables` |
| `ssh.enabled` | bool | `true` | SSH service/package management |
| `zstd.enabled` | bool | `true` | zstd-related tuning |
| `swap.enabled` | bool | `true` | Swap setup |
| `banner.motd` | bool | `false` | MOTD banner |
| `banner.sudo` | bool | `true` | Sudo banner |
| `chroot.tool` | string | `arch-chroot` | `arch-chroot`, `chroot`, or `systemd-nspawn` |
### 4.3 `hypervisor` Dictionary
| Key | Type | Description |
| ------------ | ------ | -------------------------------------------------------- |
| `type` | string | `libvirt`, `proxmox`, `vmware`, `xen`, or `none` |
| `url` | string | Proxmox/VMware API host |
| `username` | string | API username |
| `password` | string | API password |
| `host` | string | Proxmox node name |
| `storage` | string | Proxmox/VMware storage identifier |
| `datacenter` | string | VMware datacenter |
| `cluster` | string | VMware cluster |
| `certs` | bool | TLS certificate validation for VMware |
| `ssh` | bool | VMware: enable SSH on guest and switch connection to SSH |
| Key | Type | Default | Description |
| ------------ | ------ | ------- | ---------------------------------------------------- |
| `type` | string | -- | `libvirt`, `proxmox`, `vmware`, `xen`, or `none` |
| `url` | string | -- | API host (Proxmox/VMware) |
| `username` | string | -- | API username |
| `password` | string | -- | API password |
| `node` | string | -- | Target compute node (Proxmox node / VMware ESXi host; mutually exclusive with `cluster` on VMware) |
| `storage` | string | -- | Storage identifier (Proxmox/VMware) |
| `datacenter` | string | -- | VMware datacenter |
| `cluster` | string | -- | VMware cluster |
| `certs` | bool | `true` | TLS certificate validation (VMware) |
| `ssh` | bool | `false` | Enable SSH on guest and switch connection (VMware) |
### 4.4 VMware Guest Operations
### 4.4 `cis` Dictionary
When `hypervisor.type: vmware` uses the `vmware_tools` connection, these Ansible connection variables are required.
When `system.features.cis.enabled: true`, the CIS role applies hardening. All values have sensible defaults; override specific keys via the `cis` dict.
| Variable | Description |
| ------------------------------- | -------------------------------------------------- |
| `ansible_vmware_tools_user` | Guest OS username for guest operations |
| `ansible_vmware_tools_password` | Guest OS password for guest operations |
| `ansible_vmware_guest_path` | VM inventory path (`/datacenter/vm/folder/name`) |
| `ansible_vmware_host` | vCenter/ESXi hostname |
| `ansible_vmware_user` | vCenter/ESXi API username |
| `ansible_vmware_password` | vCenter/ESXi API password |
| `ansible_vmware_validate_certs` | Enable/disable TLS certificate validation |
| Key | Type | Default | Description |
| -------------------- | ------ | ------- | ------------------------------------------------ |
| `modules_blacklist` | list | see below | Kernel modules to blacklist via modprobe |
| `sysctl` | dict | see below | Sysctl key/value pairs written to `10-cis.conf` |
| `sshd_options` | list | see below | SSHD options applied via lineinfile |
| `pwquality_minlen` | int | `14` | Minimum password length |
| `tmout` | int | `900` | Shell timeout (seconds) |
| `umask` | string | `077` | Default umask in bashrc |
| `umask_profile` | string | `027` | Default umask in /etc/profile |
| `faillock_deny` | int | `5` | Failed login attempts before lockout |
| `faillock_unlock_time` | int | `900` | Lockout duration (seconds) |
| `password_remember` | int | `5` | Password history depth |
### 4.5 Multi-Disk Schema
**Default modules blacklist:** `freevxfs`, `jffs2`, `hfs`, `hfsplus`, `cramfs`, `udf`, `usb-storage`, `dccp`, `sctp`, `rds`, `tipc`, `firewire-core`, `firewire-sbp2`, `thunderbolt`. `squashfs` is added automatically except on Ubuntu (snap dependency).
`system.disks[0]` is always the OS disk. Additional entries define data disks.
**Default sysctl settings** include: `kernel.yama.ptrace_scope=2`, `kernel.kptr_restrict=2`, `kernel.perf_event_paranoid=3`, `kernel.unprivileged_bpf_disabled=1`, IPv4/IPv6 hardening, ARP protection, and IPv6 disabled by default. Override individual keys:
| Key | Type | Description |
| ------------- | ------ | ---------------------------------------------------- |
| `size` | number | Disk size in GB (required for virtual installs) |
| `device` | string | Explicit block device (required for physical data disks) |
| `mount.path` | string | Mount point (for additional disks) |
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
| `mount.label` | string | Optional filesystem label |
| `mount.opts` | string | Mount options (default: `defaults`) |
```yaml
cis:
sysctl:
net.ipv6.conf.all.disable_ipv6: 0 # re-enable IPv6
net.ipv4.ip_forward: 1 # enable for routers/containers
```
Virtual install example:
**Default SSHD options** enforce: `PermitRootLogin no`, `PasswordAuthentication no`, `X11Forwarding no`, `AllowTcpForwarding no`, `MaxAuthTries 4`, and post-quantum KEX (mlkem768x25519-sha256 on OpenSSH 9.9+). Override per-option:
```yaml
cis:
sshd_options:
- { option: X11Forwarding, value: "yes" }
- { option: AllowTcpForwarding, value: "yes" }
```
Note: providing `sshd_options` replaces the entire list. Copy the defaults from `roles/cis/defaults/main.yml` and modify as needed.
### 4.5 VMware Guest Operations
When `hypervisor.type: vmware` uses the `vmware_tools` connection:
| Variable | Description |
| ------------------------------- | -------------------------------------------- |
| `ansible_vmware_tools_user` | Guest OS username |
| `ansible_vmware_tools_password` | Guest OS password |
| `ansible_vmware_guest_path` | VM inventory path |
| `ansible_vmware_host` | vCenter/ESXi hostname |
| `ansible_vmware_user` | vCenter/ESXi API username |
| `ansible_vmware_password` | vCenter/ESXi API password |
| `ansible_vmware_validate_certs` | TLS certificate validation |
### 4.6 Multi-Disk Schema
`system.disks[0]` is the OS disk (no `mount.path`). Additional entries define data disks.
| Key | Type | Description |
| ------------- | ------ | ------------------------------------------------------ |
| `size` | number | Disk size in GB (required for virtual) |
| `device` | string | Block device path (required for physical data disks) |
| `partition` | string | Partition device path (required for physical data disks) |
| `mount.path` | string | Mount point (additional disks only) |
| `mount.fstype`| string | `btrfs`, `ext4`, or `xfs` |
| `mount.label` | string | Filesystem label |
| `mount.opts` | string | Mount options (default: `defaults`) |
```yaml
system:
disks:
- size: 80
- size: 200
- size: 80 # OS disk
- size: 200 # Data disk
mount:
path: /data
fstype: xfs
label: DATA
opts: defaults,noatime
- size: 300
mount:
path: /backup
fstype: ext4
```
Physical install example (device paths required):
### 4.7 Advanced Partitioning Overrides
```yaml
system:
type: physical
disks:
- device: /dev/sda
size: 120
- device: /dev/sdb
size: 500
mount:
path: /data
fstype: ext4
```
| Variable | Default | Description |
| ------------------------------ | ------------ | ---------------------------------------- |
| `partitioning_efi_size_mib` | `512` | EFI system partition size in MiB |
| `partitioning_boot_size_mib` | `1024` | Separate `/boot` size in MiB |
| `partitioning_separate_boot` | auto-derived | Force a separate `/boot` partition |
| `partitioning_boot_fs_fstype` | auto-derived | Filesystem for `/boot` |
| `partitioning_use_full_disk` | `true` | Use remaining VG space for root LV |
### 4.6 Advanced Partitioning Overrides
**Swap sizing:** RAM >= 16GB gets swap = RAM/2. RAM < 16GB gets swap = max(RAM_GB, 2GB). Further capped to prevent over-allocation on small disks.
Use these only when you need to override the default partition layout logic.
**LVM layout** (when not using btrfs): root, swap, and when CIS is enabled: `/home` (2-20GB, 10% of disk), `/var` (2GB), `/var/log` (2GB), `/var/log/audit` (1.5GB).
| Variable | Description | Default |
| ------------------------------ | ------------------------------------------------- | ------------ |
| `partitioning_efi_size_mib` | EFI system partition size in MiB | `512` |
| `partitioning_boot_size_mib` | Separate `/boot` size in MiB (when used) | `1024` |
| `partitioning_separate_boot` | Force a separate `/boot` partition | auto-derived |
| `partitioning_boot_fs_fstype` | Filesystem for `/boot` when separate | auto-derived |
| `partitioning_use_full_disk` | Consume remaining VG space for root LV | `true` |
### 4.8 Cleanup Defaults
## 5. How to Use the Playbook
Post-install verification and recovery settings.
### 5.1 Prerequisites
| Variable | Default | Description |
| --------------------------- | ------- | ----------------------------------------------------- |
| `cleanup_verify_boot` | `true` | Check VM accessibility after reboot |
| `cleanup_boot_timeout` | `300` | Timeout in seconds for boot verification |
| `cleanup_remove_on_failure` | `true` | Auto-remove VMs that fail to boot (created this run only) |
- Ansible installed on the control machine.
- Inventory file with target systems defined and variables configured.
- Disposable/non-production targets (the playbook enforces production-safety checks).
## 5. Execution Pipeline
### 5.2 Running the Playbook
Roles execute in this order:
Execute the playbook using `ansible-playbook`, ensuring that all necessary variables are defined either in the inventory, in a vars file, or passed via `-e`. Credentials (`root_password`, `user_name`, `user_password`, `user_public_key`) are prompted interactively unless supplied through inventory or extra vars.
1. **global_defaults** -- normalize inputs, validate, set OS flags
2. **system_check** -- detect installer environment, verify live/non-prod target
3. **virtualization** -- create VM (if virtual), attach disks, cloud-init
4. **environment** -- prepare installer: mount ISO, configure repos, setup pacman
5. **partitioning** -- create partitions, LVM, LUKS, mount filesystems
6. **bootstrap** -- install base system and packages (OS-specific)
7. **configuration** -- users, fstab, locales, bootloader, encryption enrollment, networking
8. **cis** -- CIS hardening (when `system.features.cis.enabled: true`)
9. **cleanup** -- unmount, shutdown installer, remove media, verify boot
## 6. Usage
```bash
ansible-playbook -i inventory.yml main.yml
ansible-playbook -i inventory.yml main.yml -e @vars_example.yml
ansible-playbook -i inventory.yml main.yml -e @vars.yml
```
### 5.3 Example Usage
All credentials (`system.users`, `system.root.password`) must be defined in inventory or passed via `-e`.
Use the bundled example files as starting points for new inventories:
Example inventory files are included:
- `inventory_example.yml` -- Proxmox virtual setup
- `inventory_libvirt_example.yml` -- libvirt virtual setup
- `inventory_baremetal_example.yml` -- bare-metal physical setup
- `vars_example.yml` -- shared variable overrides
- `vars_baremetal_example.yml` -- bare-metal variable overrides
```bash
# Proxmox example
ansible-playbook -i inventory_example.yml main.yml
## 7. Security
# libvirt example
ansible-playbook -i inventory_libvirt_example.yml main.yml
# Custom inventory with separate vars file
ansible-playbook -i inventory.yml main.yml -e @vars_example.yml
```
## 6. Security
To protect sensitive information such as passwords, API keys, and other confidential variables (e.g. `hypervisor.password`, `system.luks.passphrase`), **use Ansible Vault** instead of plaintext inventory files.
## 7. Operational Notes
- For virtual installs, `system.cpus`, `system.memory`, and `system.disks[0].size` are required and validated.
- For physical installs, sizing is derived from the detected install drive; set installer access (`ansible_user`/`ansible_password`) when the installer environment differs from the prompted user credentials.
- `system.network.dns.servers` and `system.network.dns.search` must be YAML lists.
- `hypervisor.type` selects backend-specific provisioning and cleanup behavior.
- Guest tools are selected automatically by hypervisor: `qemu-guest-agent` for `libvirt`/`proxmox`, `open-vm-tools` for `vmware`.
- With `system.luks.method: tpm2` on virtual installs, the virtualization role enables a TPM2 device where supported (libvirt/proxmox/vmware).
- With LUKS enabled on non-Arch targets, provisioning uses an ESP (512 MiB), a separate `/boot` (1 GiB), and the encrypted root; adjust sizes via `partitioning_efi_size_mib` and `partitioning_boot_size_mib` if needed.
- For VMware, `hypervisor.ssh: true` enables SSH on the guest and switches the connection to SSH for the remaining tasks.
- Molecule is scaffolded with a delegated driver and a no-op converge for lint-only validation.
Use **Ansible Vault** for all sensitive values (`hypervisor.password`, `system.luks.passphrase`, user passwords in `system.users`, `system.root.password`).
## 8. Safety
This playbook intentionally aborts if it detects a non-live/production target. It also refuses to touch pre-existing VMs and only cleans up VMs created in the current run.
Always run lint after changes:
```bash
ansible-lint
```
The playbook aborts on non-live/production targets. It refuses to touch pre-existing VMs and only cleans up VMs created in the current run.

View File

@@ -1,2 +1,5 @@
[defaults]
hash_behaviour = merge
interpreter_python = auto_silent
deprecation_warnings = False
host_key_checking = False

View File

@@ -1,9 +1,16 @@
---
collections:
- name: ansible.posix
version: "2.1.0"
- name: community.general
version: "12.3.0"
- name: community.libvirt
version: "2.0.0"
- name: community.crypto
version: "3.1.0"
- name: community.proxmox
version: "1.5.0"
- name: community.vmware
version: "6.2.0"
- name: vmware.vmware
version: "2.7.0"

View File

@@ -6,7 +6,7 @@ all:
url: "pve01.example.com"
username: "root@pam"
password: "CHANGE_ME"
host: "pve01"
node: "pve01"
storage: "local-lvm"
boot_iso: "local:iso/archlinux-x86_64.iso"
children:

207
main.yml
View File

@@ -1,96 +1,20 @@
---
# Bootstrap pipeline — role execution order:
# 1. global_defaults — normalize + validate system/hypervisor/disk input
# 2. system_check — pre-flight hardware/environment safety checks
# 3. virtualization — create VM on hypervisor (libvirt/proxmox/vmware/xen)
# 4. environment — detect live ISO, configure installer network, install tools
# 5. partitioning — partition disk, create FS, LUKS, LVM, mount everything
# 6. bootstrap — debootstrap/pacstrap/dnf install the target OS into /mnt
# 7. configuration — users, network, encryption, fstab, bootloader, services
# 8. cis — CIS hardening (optional, per system.features.cis.enabled)
# 9. cleanup — unmount, remove cloud-init artifacts, reboot/shutdown
- name: Create and configure VMs
hosts: all
hosts: "{{ bootstrap_target | default('all') }}"
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
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
@@ -99,32 +23,79 @@
ansible.builtin.import_role:
name: system_check
roles:
- role: virtualization
when: system_cfg.type == "virtual"
become: false
vars:
ansible_connection: local
tasks:
- name: Bootstrap pipeline
block:
- name: Record that no pre-existing VM was found
ansible.builtin.set_fact:
_vm_absent_before_bootstrap: true
- role: environment
vars:
ansible_connection: "{{ 'vmware_tools' if hypervisor_type == 'vmware' else 'ssh' }}"
- name: Create virtual machine
when: system_cfg.type == "virtual"
ansible.builtin.include_role:
name: virtualization
public: true
vars:
ansible_connection: local
ansible_become: false
- role: partitioning
vars:
partitioning_boot_partition_suffix: 1
partitioning_main_partition_suffix: 2
- name: Configure environment
ansible.builtin.include_role:
name: environment
public: true
- role: bootstrap
- name: Partition disks
ansible.builtin.include_role:
name: partitioning
public: true
vars:
partitioning_boot_partition_suffix: 1
partitioning_main_partition_suffix: 2
- role: configuration
- name: Install base system
ansible.builtin.include_role:
name: bootstrap
public: true
- role: cis
when: system_cfg.features.cis.enabled | bool
- name: Apply system configuration
ansible.builtin.include_role:
name: configuration
public: true
- role: cleanup
when: system_cfg.type in ["virtual", "physical"]
become: false
- name: Apply CIS hardening
when: system_cfg.features.cis.enabled | bool
ansible.builtin.include_role:
name: cis
public: true
- name: Clean up and finalize
when: system_cfg.type in ["virtual", "physical"]
ansible.builtin.include_role:
name: cleanup
public: true
rescue:
- name: Delete VM on bootstrap failure
when:
- _vm_absent_before_bootstrap | default(false) | bool
- virtualization_vm_created_in_run | default(false) | bool
- system_cfg.type == "virtual"
ansible.builtin.include_role:
name: virtualization
tasks_from: delete
vars:
ansible_connection: local
ansible_become: false
tags:
- rescue_cleanup
- name: Fail host after bootstrap rescue
ansible.builtin.fail:
msg: >-
Bootstrap failed for {{ hostname }}.
{{ 'VM was deleted to allow clean retry.'
if (virtualization_vm_created_in_run | default(false))
else 'VM was not created in this run (kept).' }}
post_tasks:
- name: Set post-reboot connection flags
@@ -147,11 +118,27 @@
- name: Set final SSH credentials for post-reboot tasks
when:
- post_reboot_can_connect | bool
no_log: true
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact:
ansible_user: "{{ system_cfg.users[0].name }}"
ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_connection: ssh
ansible_host: "{{ system_cfg.network.ip }}"
ansible_port: 22
ansible_user: "{{ _primary.key }}"
ansible_password: "{{ _primary.value.password }}"
ansible_become_password: "{{ _primary.value.password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
ansible_python_interpreter: /usr/bin/python3
- name: Re-gather facts for target OS after reboot
when:
- post_reboot_can_connect | bool
ansible.builtin.setup:
gather_subset:
- "!all"
- min
- pkg_mgr
- name: Install post-reboot packages
when:

View File

@@ -0,0 +1,15 @@
---
# OS → task file mapping for bootstrap dispatch.
# Each key matches a supported `os` value; value is the task file to include.
bootstrap_os_task_map:
almalinux: _dnf_family.yml
alpine: alpine.yml
archlinux: archlinux.yml
debian: debian.yml
fedora: _dnf_family.yml
opensuse: opensuse.yml
rocky: _dnf_family.yml
rhel: rhel.yml
ubuntu: ubuntu.yml
ubuntu-lts: ubuntu.yml
void: void.yml

View File

@@ -0,0 +1,47 @@
---
- name: "Bootstrap {{ os | capitalize }}"
vars:
_dnf_config: "{{ lookup('vars', bootstrap_var_key) }}"
_dnf_repos: "{{ _dnf_config.repos | map('regex_replace', '^', '--repo=') | join(' ') }}"
_dnf_groups: "{{ _dnf_config.base | join(' ') }}"
_dnf_extra: >-
{{
((_dnf_config.extra | default([])) + (_dnf_config.conditional | default([])))
| reject('equalto', '')
| join(' ')
}}
block:
- name: "Install base system for {{ os | capitalize }}"
ansible.builtin.command: >-
dnf --releasever={{ os_version }} --best {{ _dnf_repos }}
--installroot=/mnt --setopt=install_weak_deps=False
groupinstall -y {{ _dnf_groups }}
register: bootstrap_dnf_base_result
changed_when: bootstrap_dnf_base_result.rc == 0
- name: Ensure chroot has DNS resolution
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
force: true
- name: Install extra packages
ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
install -y {{ _dnf_extra }}
register: bootstrap_dnf_extra_result
changed_when: bootstrap_dnf_extra_result.rc == 0
- name: Detect installed kernel package name
ansible.builtin.command: "{{ chroot_command }} rpm -q kernel-core"
register: bootstrap_dnf_kernel_check
changed_when: false
failed_when: false
- name: Reinstall kernel package
vars:
_kernel_pkg: "{{ 'kernel-core' if bootstrap_dnf_kernel_check.rc == 0 else 'kernel' }}"
ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y {{ _kernel_pkg }}"
register: bootstrap_dnf_kernel_result
changed_when: bootstrap_dnf_kernel_result.rc == 0

View File

@@ -0,0 +1,11 @@
---
# Resolve the OS-specific variable namespace and task file for the bootstrap role.
- name: Validate OS is supported for bootstrap
ansible.builtin.assert:
that:
- os is defined
- os in bootstrap_os_task_map
fail_msg: >-
Unsupported OS '{{ os | default("undefined") }}' for bootstrap.
Supported: {{ bootstrap_os_task_map | dict2items | map(attribute='key') | join(', ') }}
quiet: true

View File

@@ -1,35 +0,0 @@
---
- name: Bootstrap AlmaLinux
vars:
bootstrap_almalinux_extra: >-
{{
lookup('vars', bootstrap_var_key)
| reject('equalto', '')
| join(' ')
}}
block:
- name: Install AlmaLinux base system
ansible.builtin.command: >-
dnf --releasever={{ os_version }} --best --repo=baseos --repo=appstream
--installroot=/mnt --setopt=install_weak_deps=False
groupinstall -y core
register: bootstrap_almalinux_base_result
changed_when: bootstrap_almalinux_base_result.rc == 0
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
- name: Install extra packages
ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
install -y {{ bootstrap_almalinux_extra }}
register: bootstrap_almalinux_extra_result
changed_when: bootstrap_almalinux_extra_result.rc == 0
- name: Reinstall kernel core
ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core"
register: bootstrap_almalinux_kernel_result
changed_when: bootstrap_almalinux_kernel_result.rc == 0

View File

@@ -1,28 +1,25 @@
---
- name: Bootstrap Alpine Linux
vars:
bootstrap_alpine_packages: >-
_config: "{{ lookup('vars', bootstrap_var_key) }}"
_base_packages: "{{ _config.base | join(' ') }}"
_extra_packages: >-
{{
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
((_config.extra | default([])) + (_config.conditional | default([])))
| reject('equalto', '')
| join(' ')
}}
block:
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
force: true
- name: Install Alpine Linux packages
- name: Install Alpine Linux base
ansible.builtin.command: >
apk --root /mnt --no-cache add alpine-base
apk --root /mnt --no-cache add {{ _base_packages }}
register: bootstrap_alpine_bootstrap_result
changed_when: bootstrap_alpine_bootstrap_result.rc == 0
- name: Install extra packages
when: bootstrap_alpine_packages | length > 0
when: _extra_packages | trim | length > 0
ansible.builtin.command: >
apk --root /mnt add {{ bootstrap_alpine_packages }}
apk --root /mnt add {{ _extra_packages }}
register: bootstrap_alpine_extra_result
changed_when: bootstrap_alpine_extra_result.rc == 0

View File

@@ -1,11 +1,14 @@
---
- name: Bootstrap ArchLinux
vars:
_config: "{{ lookup('vars', bootstrap_var_key) }}"
bootstrap_archlinux_packages: >-
{{
lookup('vars', bootstrap_var_key)
((_config.base | default([])) + (_config.conditional | default([])))
| reject('equalto', '')
| list
}}
ansible.builtin.command: >-
pacstrap /mnt {{ bootstrap_archlinux_packages | reject('equalto', '') | join(' ') }} --asexplicit
pacstrap /mnt {{ bootstrap_archlinux_packages | join(' ') }}
register: bootstrap_result
changed_when: bootstrap_result.rc == 0

View File

@@ -10,53 +10,58 @@
else 'sid' if (os_version | string) == 'unstable'
else 'trixie'
}}
bootstrap_debian_package_config: >-
{{
lookup('vars', bootstrap_var_key)
}}
bootstrap_debian_base_packages: >-
{{
bootstrap_debian_package_config.base
| default([])
| reject('equalto', '')
| list
}}
bootstrap_debian_extra_packages: >-
{{
bootstrap_debian_package_config.extra
| default([])
| reject('equalto', '')
| list
}}
bootstrap_debian_base_csv: "{{ bootstrap_debian_base_packages | join(',') }}"
_config: "{{ lookup('vars', bootstrap_var_key) }}"
bootstrap_debian_base_csv: "{{ (['ca-certificates'] + _config.base) | unique | join(',') }}"
bootstrap_debian_extra_args: >-
{{
bootstrap_debian_extra_packages
((_config.extra | default([])) + (_config.conditional | default([])))
| reject('equalto', '')
| join(' ')
}}
block:
- name: Validate Debian package configuration
ansible.builtin.assert:
that:
- bootstrap_debian_package_config is mapping
- bootstrap_debian_package_config.base is defined
- bootstrap_debian_package_config.base is sequence
- bootstrap_debian_package_config.base is not string
- bootstrap_debian_package_config.extra is defined
- bootstrap_debian_package_config.extra is sequence
- bootstrap_debian_package_config.extra is not string
fail_msg: "bootstrap package definition for {{ bootstrap_var_key }} must be a mapping with base/extra lists."
- _config is mapping
- _config.base is sequence
- _config.extra is sequence
fail_msg: "{{ bootstrap_var_key }} must be a dict with base/extra/conditional keys."
quiet: true
- name: Install Debian base system
ansible.builtin.command: >-
debootstrap --include={{ bootstrap_debian_base_csv }}
{{ bootstrap_debian_release }} /mnt http://deb.debian.org/debian/
{{ bootstrap_debian_release }} /mnt {{ system_cfg.mirror }}
register: bootstrap_debian_base_result
changed_when: bootstrap_debian_base_result.rc == 0
- name: Write bootstrap sources.list
ansible.builtin.template:
src: debian.sources.list.j2
dest: /mnt/etc/apt/sources.list
mode: "0644"
- name: Configure apt performance tuning
ansible.builtin.copy:
dest: /mnt/etc/apt/apt.conf.d/99performance
content: |
Acquire::Retries "3";
Acquire::http::Pipeline-Depth "10";
APT::Install-Recommends "false";
mode: "0644"
- name: Update package lists
ansible.builtin.command: "{{ chroot_command }} apt update"
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_packages | length > 0
when: bootstrap_debian_extra_args | trim | length > 0
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_debian_extra_args }}"
register: bootstrap_debian_extra_result
changed_when: bootstrap_debian_extra_result.rc == 0

View File

@@ -1,35 +0,0 @@
---
- name: Bootstrap Fedora
vars:
bootstrap_fedora_extra: >-
{{
lookup('vars', bootstrap_var_key)
| reject('equalto', '')
| join(' ')
}}
block:
- name: Install Fedora base system
ansible.builtin.command: >-
dnf --releasever={{ os_version }} --best --repo=fedora --repo=fedora-updates
--installroot=/mnt --setopt=install_weak_deps=False
groupinstall -y critical-path-base core
register: bootstrap_fedora_base_result
changed_when: bootstrap_fedora_base_result.rc == 0
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
- name: Install extra packages
ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
install -y {{ bootstrap_fedora_extra }}
register: bootstrap_fedora_extra_result
changed_when: bootstrap_fedora_extra_result.rc == 0
- name: Reinstall kernel core
ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core"
register: bootstrap_fedora_kernel_result
changed_when: bootstrap_fedora_kernel_result.rc == 0

View File

@@ -1,17 +1,42 @@
---
- name: Validate bootstrap input
ansible.builtin.import_tasks: _validate.yml
- name: Create API filesystem mountpoints in installroot
when: os_family == 'RedHat'
ansible.builtin.file:
path: "/mnt/{{ item }}"
state: directory
mode: "0755"
loop:
- dev
- proc
- sys
- name: Mount API filesystems into installroot
when: os_family == 'RedHat'
ansible.posix.mount:
src: "{{ item.src }}"
path: "/mnt/{{ item.path }}"
fstype: "{{ item.fstype }}"
opts: "{{ item.opts | default(omit) }}"
state: ephemeral
loop:
- { src: proc, path: proc, fstype: proc }
- { src: sysfs, path: sys, fstype: sysfs }
- { src: /dev, path: dev, fstype: none, opts: bind }
- { src: devpts, path: dev/pts, fstype: devpts, opts: "gid=5,mode=620" }
loop_control:
label: "{{ item.path }}"
- name: Run OS-specific bootstrap process
vars:
bootstrap_os_task_map:
almalinux: almalinux.yml
alpine: alpine.yml
archlinux: archlinux.yml
debian: debian.yml
fedora: fedora.yml
opensuse: opensuse.yml
rocky: rocky.yml
rhel: rhel.yml
ubuntu: ubuntu.yml
ubuntu-lts: ubuntu.yml
void: void.yml
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"
- name: Ensure chroot uses live environment DNS
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
force: true

View File

@@ -1,28 +1,25 @@
---
- name: Bootstrap openSUSE
vars:
bootstrap_opensuse_packages: >-
_config: "{{ lookup('vars', bootstrap_var_key) }}"
_base_patterns: "{{ _config.base | join(' ') }}"
_extra_packages: >-
{{
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
((_config.extra | default([])) + (_config.conditional | default([])))
| reject('equalto', '')
| join(' ')
}}
block:
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
force: true
- name: Install openSUSE base packages
- name: Install openSUSE base patterns
ansible.builtin.command: >
zypper --root /mnt --non-interactive install -t pattern patterns-base-base
zypper --root /mnt --non-interactive install -t pattern {{ _base_patterns }}
register: bootstrap_opensuse_base_result
changed_when: bootstrap_opensuse_base_result.rc == 0
- name: Install openSUSE extra packages
when: bootstrap_opensuse_packages | length > 0
- name: Install extra packages
when: _extra_packages | trim | length > 0
ansible.builtin.command: >
zypper --root /mnt --non-interactive install {{ bootstrap_opensuse_packages }}
zypper --root /mnt --non-interactive install {{ _extra_packages }}
register: bootstrap_opensuse_extra_result
changed_when: bootstrap_opensuse_extra_result.rc == 0

View File

@@ -1,20 +1,27 @@
---
- name: Bootstrap RHEL System
vars:
_rhel_config: "{{ lookup('vars', bootstrap_var_key) }}"
_rhel_repos: "{{ _rhel_config.repos | map('regex_replace', '^', '--repo=') | join(' ') }}"
_rhel_groups: "{{ _rhel_config.base | join(' ') }}"
_rhel_extra: >-
{{
((_rhel_config.extra | default([])) + (_rhel_config.conditional | default([])))
| reject('equalto', '')
| join(' ')
}}
block:
- name: Install base packages in chroot environment
ansible.builtin.command: >-
dnf --releasever={{ os_version_major }} --repo=rhel{{ os_version_major }}-baseos
dnf --releasever={{ os_version_major }} --best {{ _rhel_repos }}
--installroot=/mnt
--setopt=install_weak_deps=False --setopt=optional_metadata_types=filelists
groupinstall -y core base standard
groupinstall -y {{ _rhel_groups }}
register: bootstrap_result
changed_when: bootstrap_result.rc == 0
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
failed_when:
- bootstrap_result.rc != 0
- "'grub2-common' not in (bootstrap_result.stderr | default(''))"
- name: Ensure chroot RHEL DVD directory exists
ansible.builtin.file:
@@ -28,7 +35,7 @@
path: /mnt/usr/local/install/redhat/dvd
fstype: none
opts: bind
state: mounted
state: ephemeral
- name: Rebuild RPM database inside chroot
ansible.builtin.command: "{{ chroot_command }} rpm --rebuilddb"
@@ -43,15 +50,8 @@
remote_src: true
- name: Install additional packages in chroot
vars:
bootstrap_rhel_extra: >-
{{
lookup('vars', bootstrap_var_key)
| reject('equalto', '')
| join(' ')
}}
ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version_major }}
--setopt=install_weak_deps=False install -y {{ bootstrap_rhel_extra }}
{{ chroot_command }} dnf --releasever={{ os_version_major }} --best
--setopt=install_weak_deps=False install -y {{ _rhel_extra }}
register: bootstrap_result
changed_when: bootstrap_result.rc == 0

View File

@@ -1,35 +0,0 @@
---
- name: Bootstrap Rocky Linux
vars:
bootstrap_rocky_extra: >-
{{
lookup('vars', bootstrap_var_key)
| reject('equalto', '')
| join(' ')
}}
block:
- name: Install Rocky Linux base system
ansible.builtin.command: >-
dnf --releasever={{ os_version }} --best --repo=baseos --repo=appstream
--installroot=/mnt --setopt=install_weak_deps=False
groupinstall -y core
register: bootstrap_rocky_base_result
changed_when: bootstrap_rocky_base_result.rc == 0
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
- name: Install extra packages
ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False
install -y {{ bootstrap_rocky_extra }}
register: bootstrap_rocky_extra_result
changed_when: bootstrap_rocky_extra_result.rc == 0
- name: Reinstall kernel core
ansible.builtin.command: "{{ chroot_command }} dnf reinstall -y kernel-core"
register: bootstrap_rocky_kernel_result
changed_when: bootstrap_rocky_kernel_result.rc == 0

View File

@@ -1,68 +1,66 @@
---
- name: Bootstrap Ubuntu System
vars:
bootstrap_ubuntu_release: >-
{{ 'plucky' if os == 'ubuntu' else 'noble' }}
bootstrap_ubuntu_package_config: >-
# ubuntu = latest non-LTS, ubuntu-lts = latest LTS
bootstrap_ubuntu_release_map:
ubuntu: questing
ubuntu-lts: noble
bootstrap_ubuntu_release: "{{ bootstrap_ubuntu_release_map[os] | default('noble') }}"
_config: "{{ lookup('vars', bootstrap_var_key) }}"
bootstrap_ubuntu_base_csv: "{{ (['ca-certificates'] + _config.base) | unique | join(',') }}"
bootstrap_ubuntu_extra_args: >-
{{
lookup('vars', bootstrap_var_key)
}}
bootstrap_ubuntu_base_packages: >-
{{
bootstrap_ubuntu_package_config.base
| default([])
((_config.extra | default([])) + (_config.conditional | default([])))
| reject('equalto', '')
| list
| join(' ')
}}
bootstrap_ubuntu_extra_packages: >-
{{
bootstrap_ubuntu_package_config.extra
| default([])
| reject('equalto', '')
| list
}}
bootstrap_ubuntu_base_csv: "{{ bootstrap_ubuntu_base_packages | join(',') }}"
bootstrap_ubuntu_extra: "{{ bootstrap_ubuntu_extra_packages | join(' ') }}"
block:
- name: Validate Ubuntu package configuration
ansible.builtin.assert:
that:
- bootstrap_ubuntu_package_config is mapping
- bootstrap_ubuntu_package_config.base is defined
- bootstrap_ubuntu_package_config.base is sequence
- bootstrap_ubuntu_package_config.base is not string
- bootstrap_ubuntu_package_config.extra is defined
- bootstrap_ubuntu_package_config.extra is sequence
- bootstrap_ubuntu_package_config.extra is not string
fail_msg: "bootstrap package definition for {{ bootstrap_var_key }} must be a mapping with base/extra lists."
- _config is mapping
- _config.base is sequence
- _config.extra is sequence
fail_msg: "{{ bootstrap_var_key }} must be a dict with base/extra/conditional keys."
quiet: true
- name: Install Ubuntu base system
ansible.builtin.command: >-
debootstrap --include={{ bootstrap_ubuntu_base_csv }}
debootstrap
--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg
--include={{ bootstrap_ubuntu_base_csv }}
{{ bootstrap_ubuntu_release }} /mnt
http://archive.ubuntu.com/ubuntu/
{{ system_cfg.mirror }}
register: bootstrap_ubuntu_base_result
changed_when: bootstrap_ubuntu_base_result.rc == 0
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
- name: Write bootstrap sources.list
ansible.builtin.template:
src: ubuntu.sources.list.j2
dest: /mnt/etc/apt/sources.list
mode: "0644"
- name: Enable universe repository
ansible.builtin.command: "{{ chroot_command }} sed -i '1s|$| universe|' /etc/apt/sources.list"
register: bootstrap_ubuntu_repo_result
changed_when: bootstrap_ubuntu_repo_result.rc == 0
- name: Configure apt performance tuning
ansible.builtin.copy:
dest: /mnt/etc/apt/apt.conf.d/99performance
content: |
Acquire::Retries "3";
Acquire::http::Pipeline-Depth "10";
APT::Install-Recommends "false";
mode: "0644"
- name: Update package lists
ansible.builtin.command: "{{ chroot_command }} apt update"
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_packages | length > 0
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra }}"
when: bootstrap_ubuntu_extra_args | trim | length > 0
ansible.builtin.command: "{{ chroot_command }} apt install -y {{ bootstrap_ubuntu_extra_args }}"
register: bootstrap_ubuntu_extra_result
changed_when: bootstrap_ubuntu_extra_result.rc == 0

View File

@@ -1,28 +1,25 @@
---
- name: Bootstrap Void Linux
vars:
bootstrap_void_packages: >-
_config: "{{ lookup('vars', bootstrap_var_key) }}"
_base_packages: "{{ _config.base | join(' ') }}"
_extra_packages: >-
{{
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
((_config.extra | default([])) + (_config.conditional | default([])))
| reject('equalto', '')
| join(' ')
}}
block:
- name: Ensure chroot has resolv.conf
ansible.builtin.file:
src: /run/NetworkManager/resolv.conf
dest: /mnt/etc/resolv.conf
state: link
force: true
- name: Install Void Linux base packages
- name: Install Void Linux base
ansible.builtin.command: >
xbps-install -Sy -r /mnt -R https://repo-default.voidlinux.org/current void-repo-nonfree base-system
xbps-install -Sy -r /mnt -R https://repo-default.voidlinux.org/current {{ _base_packages }}
register: bootstrap_void_base_result
changed_when: bootstrap_void_base_result.rc == 0
- name: Install extra packages
when: bootstrap_void_packages | length > 0
when: _extra_packages | trim | length > 0
ansible.builtin.command: >
xbps-install -Su -r /mnt {{ bootstrap_void_packages }}
xbps-install -Su -r /mnt {{ _extra_packages }}
register: bootstrap_void_extra_result
changed_when: bootstrap_void_extra_result.rc == 0

View File

@@ -0,0 +1,15 @@
# Managed by Ansible.
{% set release = bootstrap_debian_release %}
{% set mirror = system_cfg.mirror %}
{% set components = 'main contrib non-free' ~ (' non-free-firmware' if (os_version | string) not in ['10', '11'] else '') %}
deb {{ mirror }} {{ release }} {{ components }}
deb-src {{ mirror }} {{ release }} {{ components }}
{% if release != 'sid' %}
deb https://security.debian.org/debian-security {{ release }}-security {{ components }}
deb-src https://security.debian.org/debian-security {{ release }}-security {{ components }}
deb {{ mirror }} {{ release }}-updates {{ components }}
deb-src {{ mirror }} {{ release }}-updates {{ components }}
{% endif %}

View File

@@ -0,0 +1,16 @@
# Managed by Ansible.
{% set release = bootstrap_ubuntu_release %}
{% set mirror = system_cfg.mirror %}
{% set components = 'main restricted universe multiverse' %}
deb {{ mirror }} {{ release }} {{ components }}
deb-src {{ mirror }} {{ release }} {{ components }}
deb {{ mirror }} {{ release }}-updates {{ components }}
deb-src {{ mirror }} {{ release }}-updates {{ components }}
deb {{ mirror }} {{ release }}-security {{ components }}
deb-src {{ mirror }} {{ release }}-security {{ components }}
deb {{ mirror }} {{ release }}-backports {{ components }}
deb-src {{ mirror }} {{ release }}-backports {{ components }}

View File

@@ -1,166 +1,397 @@
---
# Common conditional packages shared across distributions.
# Arch overrides nftables with iptables-nft; SSH package names vary per distro.
bootstrap_common_conditional:
- "{{ 'firewalld' if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else '' }}"
- "{{ 'ufw' if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else '' }}"
- "{{ 'iptables' if system_cfg.features.firewall.toolkit == 'iptables' else '' }}"
- "{{ 'nftables' if system_cfg.features.firewall.toolkit == 'nftables' else '' }}"
- "{{ 'cryptsetup' if system_cfg.luks.enabled else '' }}"
- "{{ 'tpm2-tools' if system_cfg.luks.enabled else '' }}"
- "{{ 'qemu-guest-agent' if hypervisor_type in ['libvirt', 'proxmox'] else '' }}"
- "{{ 'open-vm-tools' if hypervisor_type == 'vmware' else '' }}"
bootstrap_rhel_base: >-
# Feature-gated packages shared across all distros.
# Arch has special nftables handling and composes this differently.
bootstrap_common_conditional: >-
{{
['bind-utils', 'dhcp-client', 'efibootmgr',
'glibc-langpack-de', 'glibc-langpack-en', 'lrzsz',
'lvm2', 'mtr', 'ncurses-term', 'nfs-utils',
'policycoreutils-python-utils', 'shim', 'tmux', 'vim', 'zstd']
+ bootstrap_common_conditional
(
(['firewalld'] if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else [])
+ (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else [])
+ (['iptables'] if system_cfg.features.firewall.toolkit == 'iptables' and system_cfg.features.firewall.enabled | bool else [])
+ (['nftables'] if system_cfg.features.firewall.toolkit == 'nftables' and system_cfg.features.firewall.enabled | bool else [])
+ (['cryptsetup', 'tpm2-tools'] if system_cfg.luks.enabled | bool else [])
+ (['qemu-guest-agent'] if hypervisor_type in ['libvirt', 'proxmox'] else [])
+ (['open-vm-tools'] if hypervisor_type == 'vmware' else [])
)
}}
bootstrap_rhel_versioned:
- grub2
- "{{ 'grub2-efi-x64' if os_version_major | default('') == '8' else 'grub2-efi' }}"
- "{{ 'grub2-tools-extra' if os_version_major | default('') in ['8', '9'] else '' }}"
- "{{ 'python39' if os_version_major | default('') == '8' else 'python' }}"
- "{{ 'kernel' if os_version_major | default('') == '10' else '' }}"
- "{{ 'zram-generator' if os_version_major | default('') in ['9', '10'] else '' }}"
# ---------------------------------------------------------------------------
# Per-OS package definitions: base (rootfs/group install), extra (post-base),
# conditional (feature/version-gated, appended by task files).
# DNF-based distros also carry repos (dnf --repo) and use base as group names.
# ---------------------------------------------------------------------------
bootstrap_rhel: "{{ bootstrap_rhel_base + bootstrap_rhel_versioned }}"
bootstrap_rhel:
repos:
- "rhel{{ os_version_major }}-baseos"
base:
- core
- base
- standard
extra:
- bind-utils
- efibootmgr
- glibc-langpack-de
- glibc-langpack-en
- grub2
- lrzsz
- lvm2
- mtr
- ncurses-term
- nfs-utils
- policycoreutils-python-utils
- shim
- tmux
- vim
- zstd
conditional: >-
{{
(['grub2-efi-x64'] if os_version_major | default('') == '8' else ['grub2-efi'])
+ (['grub2-tools-extra'] if os_version_major | default('') in ['8', '9'] else [])
+ (['dhcp-client'] if (os_version_major | default('9') | int) < 10 else [])
+ (['python39'] if os_version_major | default('') == '8' else ['python'])
+ (['kernel'] if os_version_major | default('') == '10' else [])
+ (['zram-generator'] if os_version_major | default('') in ['9', '10'] else [])
+ bootstrap_common_conditional
}}
bootstrap_almalinux: >-
{{
bootstrap_rhel_base
+ ['grub2', 'grub2-efi', 'dbus-daemon', 'lrzsz',
'nfsv4-client-utils', 'nc', 'ppp', 'zram-generator']
}}
bootstrap_almalinux:
repos:
- baseos
- appstream
base:
- core
extra:
- bind-utils
- efibootmgr
- glibc-langpack-de
- glibc-langpack-en
- grub2
- grub2-efi
- kernel
- lrzsz
- lvm2
- mtr
- nc
- ncurses-term
- nfs-utils
- nfsv4-client-utils
- policycoreutils-python-utils
- ppp
- python3
- shim
- tmux
- vim
- zram-generator
- zstd
conditional: >-
{{
(['dbus-daemon'] if (os_version_major | default('10') | int) >= 9 else [])
+ (['dhcp-client'] if (os_version_major | default('10') | int) < 10 else [])
+ bootstrap_common_conditional
}}
bootstrap_rocky: >-
{{
bootstrap_rhel_base
+ ['grub2', 'grub2-efi', 'nfsv4-client-utils', 'nc', 'ppp',
'telnet', 'util-linux-core', 'wget', 'zram-generator']
}}
bootstrap_rocky:
repos:
- baseos
- appstream
base:
- core
extra:
- bind-utils
- efibootmgr
- glibc-langpack-de
- glibc-langpack-en
- grub2
- grub2-efi
- kernel
- lrzsz
- lvm2
- mtr
- nc
- ncurses-term
- nfs-utils
- nfsv4-client-utils
- policycoreutils-python-utils
- ppp
- python3
- shim
- telnet
- tmux
- util-linux-core
- vim
- wget
- zram-generator
- zstd
conditional: >-
{{
(['dhcp-client'] if (os_version_major | default('9') | int) < 10 else [])
+ bootstrap_common_conditional
}}
bootstrap_fedora: >-
{{
['bat', 'bind-utils', 'btrfs-progs', 'cronie', 'dhcp-client',
'duf', 'efibootmgr', 'entr', 'fish', 'fzf',
'glibc-langpack-de', 'glibc-langpack-en', 'grub2', 'grub2-efi',
'htop', 'iperf3', 'logrotate', 'lrzsz', 'lvm2',
'nc', 'nfs-utils', 'nfsv4-client-utils', 'polkit', 'ppp',
'ripgrep', 'shim', 'tmux', 'vim-default-editor',
'wget', 'zoxide', 'zram-generator', 'zstd']
+ bootstrap_common_conditional
}}
bootstrap_debian_base_common:
- btrfs-progs
- cron
- gnupg
- grub-efi
- grub-efi-amd64-signed
- grub2-common
- "{{ 'cryptsetup' if system_cfg.luks.enabled else '' }}"
- "{{ 'cryptsetup-initramfs' if system_cfg.luks.enabled else '' }}"
- locales
- logrotate
- lvm2
- "{{ 'iptables' if system_cfg.features.firewall.toolkit == 'iptables' else '' }}"
- "{{ 'nftables' if system_cfg.features.firewall.toolkit == 'nftables' else '' }}"
- "{{ 'openssh-server' if system_cfg.features.ssh.enabled | bool else '' }}"
- python3
- xfsprogs
bootstrap_debian_extra_common:
- apparmor-utils
- bat
- chrony
- curl
- entr
- fish
- fzf
- htop
- jq
- libpam-pwquality
- lrzsz
- mtr
- ncdu
- net-tools
- network-manager
- python-is-python3
- ripgrep
- rsync
- screen
- sudo
- syslog-ng
- tcpd
- vim
- wget
- zstd
bootstrap_debian_extra_versioned:
- linux-image-amd64
- "{{ 'duf' if (os_version | string) not in ['10', '11'] else '' }}"
- "{{ 'fastfetch' if (os_version | string) in ['12', '13', 'unstable'] else '' }}"
- "{{ 'neofetch' if (os_version | string) == '12' else '' }}"
- "{{ '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 '' }}"
bootstrap_fedora:
repos:
- fedora
- fedora-updates
base:
- critical-path-base
- core
extra:
- bat
- bind-utils
- btrfs-progs
- cronie
- dhcp-client
- duf
- efibootmgr
- entr
- fish
- fzf
- glibc-langpack-de
- glibc-langpack-en
- grub2
- grub2-efi
- htop
- iperf3
- logrotate
- lrzsz
- lvm2
- nc
- nfs-utils
- nfsv4-client-utils
- polkit
- ppp
- python3
- ripgrep
- shim
- tmux
- vim-default-editor
- wget
- zoxide
- zram-generator
- zstd
conditional: "{{ bootstrap_common_conditional }}"
bootstrap_debian:
base: "{{ bootstrap_debian_base_common }}"
extra: >-
base:
- btrfs-progs
- cron
- cryptsetup-initramfs
- gnupg
- grub-efi
- grub-efi-amd64-signed
- grub2-common
- locales
- logrotate
- lvm2
- openssh-server
- python3
- xfsprogs
extra:
- apparmor-utils
- bat
- chrony
- curl
- entr
- fish
- fzf
- htop
- jq
- libpam-pwquality
- linux-image-amd64
- lrzsz
- mtr
- ncdu
- needrestart
- net-tools
- network-manager
- python-is-python3
- ripgrep
- rsync
- screen
- sudo
- syslog-ng
- tcpd
- vim
- wget
- zstd
conditional: >-
{{
bootstrap_debian_extra_common
+ bootstrap_debian_extra_versioned
(['duf'] if (os_version | string) not in ['10', '11'] else [])
+ (['fastfetch'] if (os_version | string) in ['13', 'unstable'] else [])
+ (['neofetch'] if (os_version | string) == '12' else [])
+ (['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 [])
+ bootstrap_common_conditional
}}
bootstrap_ubuntu:
base:
- btrfs-progs
- cron
- cryptsetup-initramfs
- gnupg
- grub-efi
- grub-efi-amd64-signed
- grub2-common
- initramfs-tools
- linux-image-generic
extra: >-
- locales
- logrotate
- lvm2
- openssh-server
- python3
- xfsprogs
extra:
- apparmor-utils
- bash-completion
- bat
- chrony
- curl
- dnsutils
- duf
- entr
- eza
- fdupes
- fio
- fish
- fzf
- htop
- jq
- libpam-pwquality
- lrzsz
- mtr
- ncdu
- ncurses-term
- needrestart
- net-tools
- network-manager
- python-is-python3
- ripgrep
- rsync
- screen
- software-properties-common
- sudo
- syslog-ng
- systemd-zram-generator
- tcpd
- traceroute
- util-linux-extra
- vim
- wget
- yq
- zoxide
- zstd
conditional: >-
{{
bootstrap_debian_base_common
+ bootstrap_debian_extra_common
+ ['bash-completion', 'dnsutils', 'duf', 'eza', 'fdupes', 'fio',
'ncurses-term', 'software-properties-common', 'systemd-zram-generator',
'tldr', 'traceroute', 'util-linux-extra', 'yq', 'zoxide']
(['tldr'] if (os_version | default('') | string | length) > 0 else [])
+ bootstrap_common_conditional
}}
bootstrap_archlinux: >-
{{
['base', 'btrfs-progs', 'cronie', 'dhcpcd', 'efibootmgr', 'fastfetch',
'fish', 'fzf', 'grub', 'htop', 'libpwquality', 'linux', 'logrotate',
'lrzsz', 'lsof', 'lvm2', 'ncdu', 'networkmanager', 'nfs-utils',
'ppp', 'prometheus-node-exporter', 'python-psycopg2', 'reflector',
'rsync', 'sudo', 'tldr', 'tmux', 'vim', 'wireguard-tools', 'zram-generator']
+ [('openssh' if system_cfg.features.ssh.enabled | bool else '')]
+ [('iptables-nft' if system_cfg.features.firewall.toolkit == 'nftables' else '')]
+ (bootstrap_common_conditional | reject('equalto', 'nftables') | list)
}}
bootstrap_archlinux:
base:
- base
- btrfs-progs
- cronie
- dhcpcd
- efibootmgr
- fastfetch
- fish
- fzf
- grub
- htop
- libpwquality
- linux
- logrotate
- lrzsz
- lsof
- lvm2
- ncdu
- networkmanager
- nfs-utils
- ppp
- python
- reflector
- rsync
- sudo
- tldr
- tmux
- vim
- zram-generator
extra: []
conditional: >-
{{
(['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 [])
+ (bootstrap_common_conditional | reject('equalto', 'nftables') | list)
}}
bootstrap_alpine: >-
{{
['alpine-base', 'vim']
+ [('openssh' if system_cfg.features.ssh.enabled | bool else '')]
+ bootstrap_common_conditional
}}
bootstrap_alpine:
base:
- alpine-base
extra:
- btrfs-progs
- chrony
- curl
- e2fsprogs
- linux-lts
- logrotate
- lvm2
- python3
- rsync
- sudo
- util-linux
- vim
- xfsprogs
conditional: >-
{{
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
+ bootstrap_common_conditional
}}
bootstrap_opensuse: >-
{{
['vim']
+ [('openssh' if system_cfg.features.ssh.enabled | bool else '')]
+ bootstrap_common_conditional
}}
bootstrap_opensuse:
base:
- patterns-base-base
extra:
- btrfs-progs
- chrony
- curl
- e2fsprogs
- glibc-locale
- kernel-default
- logrotate
- lvm2
- NetworkManager
- python3
- rsync
- sudo
- vim
- xfsprogs
conditional: >-
{{
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
+ bootstrap_common_conditional
}}
bootstrap_void: >-
{{
['vim']
+ [('openssh' if system_cfg.features.ssh.enabled | bool else '')]
+ bootstrap_common_conditional
}}
bootstrap_void:
base:
- base-system
- void-repo-nonfree
extra:
- btrfs-progs
- chrony
- curl
- dhcpcd
- e2fsprogs
- logrotate
- lvm2
- python3
- rsync
- sudo
- vim
- xfsprogs
conditional: >-
{{
(['openssh'] if system_cfg.features.ssh.enabled | bool else [])
+ bootstrap_common_conditional
}}

View File

@@ -1,21 +1,100 @@
---
cis_permission_targets: >-
{{
[
{ "path": "/mnt/etc/ssh/sshd_config", "mode": "0600" },
{ "path": "/mnt/etc/cron.hourly", "mode": "0700" },
{ "path": "/mnt/etc/cron.daily", "mode": "0700" },
{ "path": "/mnt/etc/cron.weekly", "mode": "0700" },
{ "path": "/mnt/etc/cron.monthly", "mode": "0700" },
{ "path": "/mnt/etc/cron.d", "mode": "0700" },
{ "path": "/mnt/etc/crontab", "mode": "0600" },
{ "path": "/mnt/etc/logrotate.conf", "mode": "0644" },
{ "path": "/mnt/usr/sbin/pppd", "mode": "0754" } if os != "rhel" else None,
{
"path": "/mnt/usr/bin/"
+ ("fusermount3" if os in ["archlinux", "fedora", "rocky"] or os == "rhel" or (os == "debian" and (os_version | string) == "12") else "fusermount"),
"mode": "755"
},
{ "path": "/mnt/usr/bin/" + ("write.ul" if os == "debian" and (os_version | string) == "11" else "write"), "mode": "755" }
] | reject("none")
}}
# User-facing API: override via top-level `cis` dict in inventory.
# Merged with these defaults in _normalize.yml → cis_cfg.
cis_defaults:
modules_blacklist:
- freevxfs
- jffs2
- hfs
- hfsplus
- cramfs
- udf
- usb-storage
- dccp
- sctp
- rds
- tipc
- firewire-core
- firewire-sbp2
- thunderbolt
sysctl:
fs.suid_dumpable: 0
kernel.dmesg_restrict: 1
kernel.kptr_restrict: 2
kernel.perf_event_paranoid: 3
kernel.unprivileged_bpf_disabled: 1
kernel.yama.ptrace_scope: 2
kernel.randomize_va_space: 2
net.ipv4.ip_forward: 0
net.ipv4.tcp_syncookies: 1
net.ipv4.icmp_echo_ignore_broadcasts: 1
net.ipv4.icmp_ignore_bogus_error_responses: 1
net.ipv4.conf.all.log_martians: 1
net.ipv4.conf.all.rp_filter: 1
net.ipv4.conf.all.secure_redirects: 0
net.ipv4.conf.all.send_redirects: 0
net.ipv4.conf.all.accept_redirects: 0
net.ipv4.conf.all.accept_source_route: 0
net.ipv4.conf.all.arp_ignore: 1
net.ipv4.conf.all.arp_announce: 2
net.ipv4.conf.default.log_martians: 1
net.ipv4.conf.default.rp_filter: 1
net.ipv4.conf.default.secure_redirects: 0
net.ipv4.conf.default.send_redirects: 0
net.ipv4.conf.default.accept_redirects: 0
net.ipv6.conf.all.accept_redirects: 0
net.ipv6.conf.all.disable_ipv6: 1
net.ipv6.conf.default.accept_redirects: 0
net.ipv6.conf.default.disable_ipv6: 1
net.ipv6.conf.lo.disable_ipv6: 1
sshd_options:
- { option: LogLevel, value: VERBOSE }
- { option: LoginGraceTime, value: "60" }
- { option: PermitRootLogin, value: "no" }
- { option: StrictModes, value: "yes" }
- { option: MaxAuthTries, value: "4" }
- { option: MaxSessions, value: "10" }
- { option: MaxStartups, value: "10:30:60" }
- { option: PubkeyAuthentication, value: "yes" }
- { option: HostbasedAuthentication, value: "no" }
- { option: IgnoreRhosts, value: "yes" }
- { option: PasswordAuthentication, value: "no" }
- { option: PermitEmptyPasswords, value: "no" }
- { option: KerberosAuthentication, value: "no" }
- { option: GSSAPIAuthentication, value: "no" }
- { option: AllowAgentForwarding, value: "no" }
- { option: AllowTcpForwarding, value: "no" }
- { option: KbdInteractiveAuthentication, value: "no" }
- { option: GatewayPorts, value: "no" }
- { option: X11Forwarding, value: "no" }
- { option: PermitUserEnvironment, value: "no" }
- { option: ClientAliveInterval, value: "300" }
- { option: ClientAliveCountMax, value: "1" }
- { option: PermitTunnel, value: "no" }
- { option: Banner, value: /etc/issue.net }
pwquality_minlen: 14
tmout: 900
umask: "077"
umask_profile: "027"
faillock_deny: 5
faillock_unlock_time: 900
password_remember: 5
# Platform-specific binary names for CIS permission targets
cis_fusermount_binary: "{{ 'fusermount3' if is_rhel | default(false) | bool else 'fusermount' }}"
cis_write_binary: "{{ 'write' if is_rhel | default(false) | bool else 'wall' }}"
cis: {}
cis_permission_targets:
- { path: "/mnt/etc/ssh/sshd_config", mode: "0600" }
- { path: "/mnt/etc/cron.hourly", mode: "0700" }
- { path: "/mnt/etc/cron.daily", mode: "0700" }
- { path: "/mnt/etc/cron.weekly", mode: "0700" }
- { path: "/mnt/etc/cron.monthly", mode: "0700" }
- { path: "/mnt/etc/cron.d", mode: "0700" }
- { path: "/mnt/etc/crontab", mode: "0600" }
- { path: "/mnt/etc/logrotate.conf", mode: "0644" }
- { path: "/mnt/usr/sbin/pppd", mode: "0754" }
- { path: "/mnt/usr/bin/{{ cis_fusermount_binary }}", mode: "0755" }
- { path: "/mnt/usr/bin/{{ cis_write_binary }}", mode: "0755" }

View File

@@ -0,0 +1,10 @@
---
- name: Normalize CIS input
ansible.builtin.set_fact:
cis_enabled: "{{ cis is defined and (cis is mapping or cis | bool) }}"
cis_input: "{{ cis if cis is mapping else {} }}"
- name: Normalize CIS configuration
when: cis_enabled and cis_cfg is not defined
ansible.builtin.set_fact:
cis_cfg: "{{ cis_defaults | combine(cis_input, recursive=True) }}"

View File

@@ -3,13 +3,21 @@
ansible.builtin.lineinfile:
path: "/mnt/etc/profile"
regexp: "^(\\s*)umask\\s+\\d+"
line: "umask 027"
line: "umask {{ cis_cfg.umask_profile }}"
# Non-RHEL/non-Debian distros: loop evaluates to [] (intentional skip)
- name: Prevent Login to Accounts With Empty Password
ansible.builtin.replace:
dest: "{{ item }}"
regexp: "\\s*nullok"
replace: ""
loop:
- /mnt/etc/pam.d/system-auth
- /mnt/etc/pam.d/password-auth
loop: >-
{{
['/mnt/etc/pam.d/system-auth', '/mnt/etc/pam.d/password-auth']
if is_rhel | bool
else (
['/mnt/etc/pam.d/common-auth', '/mnt/etc/pam.d/common-password']
if is_debian | bool
else []
)
}}

View File

@@ -1,6 +1,8 @@
---
# Fedora ships its own crypto-policies preset and update-crypto-policies
# behaves differently; applying DEFAULT:NO-SHA1 can break package signing.
- name: Configure System Cryptography Policy
when: os == "rhel" or os in ["almalinux", "rocky"]
when: os in (os_family_rhel | difference(['fedora']))
ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set DEFAULT:NO-SHA1"
register: cis_crypto_policy_result
changed_when: "'Setting system-wide crypto-policies to' in cis_crypto_policy_result.stdout"
@@ -9,4 +11,4 @@
ansible.builtin.command: >
{{ chroot_command }} systemctl mask {{ 'nftables' if system_cfg.features.firewall.toolkit == 'iptables' else 'iptables' }} bluetooth rpcbind
register: cis_mask_services_result
changed_when: cis_mask_services_result.rc == 0
changed_when: "'Created symlink' in cis_mask_services_result.stderr"

View File

@@ -1,14 +1,20 @@
---
- name: Include CIS hardening tasks
ansible.builtin.include_tasks: "{{ cis_task }}"
loop:
- modules.yml
- sysctl.yml
- auth.yml
- crypto.yml
- files.yml
- security_lines.yml
- permissions.yml
- sshd.yml
loop_control:
loop_var: cis_task
- name: Normalize CIS configuration
ansible.builtin.import_tasks: _normalize.yml
- name: Apply CIS hardening
when: cis_enabled
block:
- name: Include CIS hardening tasks
ansible.builtin.include_tasks: "{{ cis_task }}"
loop:
- modules.yml
- sysctl.yml
- auth.yml
- crypto.yml
- files.yml
- security_lines.yml
- permissions.yml
- sshd.yml
loop_control:
loop_var: cis_task

View File

@@ -1,22 +1,17 @@
---
- name: Disable Kernel Modules
vars:
# Ubuntu uses squashfs for snap packages — blacklisting it breaks snap entirely
cis_modules_squashfs: "{{ [] if os in ['ubuntu', 'ubuntu-lts'] else ['squashfs'] }}"
cis_modules_all: "{{ cis_cfg.modules_blacklist + cis_modules_squashfs }}"
ansible.builtin.copy:
dest: /mnt/etc/modprobe.d/cis.conf
mode: "0644"
content: |
# CIS LVL 3 Restrictions
install freevxfs /bin/false
install jffs2 /bin/false
install hfs /bin/false
install hfsplus /bin/false
install cramfs /bin/false
install squashfs /bin/false
install udf /bin/false
install usb-storage /bin/false
install dccp /bin/false
install sctp /bin/false
install rds /bin/false
install tipc /bin/false
{% for mod in cis_modules_all %}
install {{ mod }}{{ ' ' * (16 - mod | length) }}/bin/false
{% endfor %}
- name: Remove old USB rules file
ansible.builtin.file:

View File

@@ -3,6 +3,8 @@
ansible.builtin.stat:
path: "{{ item.path }}"
loop: "{{ cis_permission_targets }}"
loop_control:
label: "{{ item.path }}"
register: cis_permission_stats
changed_when: false
@@ -13,4 +15,6 @@
group: "{{ item.item.group | default(omit) }}"
mode: "{{ item.item.mode }}"
loop: "{{ cis_permission_stats.results }}"
loop_control:
label: "{{ item.item.path }}"
when: item.stat.exists

View File

@@ -2,19 +2,30 @@
- name: Add Security related lines into config files
ansible.builtin.lineinfile:
path: "{{ item.path }}"
regexp: "{{ item.regexp }}"
line: "{{ item.content }}"
loop:
- { path: /mnt/etc/security/limits.conf, content: "* hard core 0" }
- { path: /mnt/etc/security/pwquality.conf, content: minlen = 14 }
- { path: /mnt/etc/security/pwquality.conf, content: dcredit = -1 }
- { path: /mnt/etc/security/pwquality.conf, content: ucredit = -1 }
- { path: /mnt/etc/security/pwquality.conf, content: ocredit = -1 }
- { path: /mnt/etc/security/pwquality.conf, content: lcredit = -1 }
- { path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: umask 077 }
- { path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: export TMOUT=3000 }
- { path: '/mnt/{{ "usr/lib/systemd/journald.conf" if os == "fedora" else "etc/systemd/journald.conf" }}', content: Storage=persistent }
- { path: /mnt/etc/sudoers, content: Defaults logfile="/var/log/sudo.log" }
- { path: /mnt/etc/pam.d/su, content: auth required pam_wheel.so }
- { path: /mnt/etc/security/limits.conf, regexp: '^\*\s+hard\s+core\s+', content: "* hard core 0" }
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*minlen\s*=', content: "minlen = {{ cis_cfg.pwquality_minlen }}" }
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*dcredit\s*=', content: dcredit = -1 }
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*ucredit\s*=', content: ucredit = -1 }
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*ocredit\s*=', content: ocredit = -1 }
- { path: /mnt/etc/security/pwquality.conf, regexp: '^\s*#?\s*lcredit\s*=', content: lcredit = -1 }
- path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
regexp: '^\s*umask\s+\d+'
content: "umask {{ cis_cfg.umask }}"
- path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}'
regexp: '^\s*(export\s+)?TMOUT='
content: "export TMOUT={{ cis_cfg.tmout }}"
- path: '/mnt/{{ "usr/lib/systemd/journald.conf" if is_rhel | bool else "etc/systemd/journald.conf" }}'
regexp: '^\s*#?\s*Storage='
content: Storage=persistent
- path: /mnt/etc/sudoers
regexp: '^\s*Defaults\s+logfile='
content: Defaults logfile="/var/log/sudo.log"
- path: /mnt/etc/pam.d/su
regexp: '^\s*#?\s*auth\s+required\s+pam_wheel\.so'
content: auth required pam_wheel.so
- path: >-
/mnt/etc/{{
"pam.d/common-auth"
@@ -23,8 +34,9 @@
if os == "fedora"
else "pam.d/system-auth"
}}
regexp: '^\s*auth\s+required\s+pam_faillock\.so'
content: >-
auth required pam_faillock.so onerr=fail audit silent deny=5 unlock_time=900
auth required pam_faillock.so onerr=fail audit silent deny={{ cis_cfg.faillock_deny }} unlock_time={{ cis_cfg.faillock_unlock_time }}
- path: >-
/mnt/etc/{{
"pam.d/common-account"
@@ -33,6 +45,7 @@
if os == "fedora"
else "pam.d/system-auth"
}}
regexp: '^\s*account\s+required\s+pam_faillock\.so'
content: account required pam_faillock.so
- path: >-
/mnt/etc/pam.d/{{
@@ -40,7 +53,10 @@
if is_debian | bool
else "passwd"
}}
regexp: '^\s*password\s+\[success=1.*\]\s+pam_unix\.so'
content: >-
password [success=1 default=ignore] pam_unix.so obscure sha512 remember=5
- { path: /mnt/etc/hosts.deny, content: "ALL: ALL" }
- { path: /mnt/etc/hosts.allow, content: "sshd: ALL" }
password [success=1 default=ignore] pam_unix.so obscure sha512 remember={{ cis_cfg.password_remember }}
- { path: /mnt/etc/hosts.deny, regexp: '^ALL:\s*ALL', content: "ALL: ALL" }
- { path: /mnt/etc/hosts.allow, regexp: '^sshd:\s*ALL', content: "sshd: ALL" }
loop_control:
label: "{{ item.content }}"

View File

@@ -4,48 +4,37 @@
path: /mnt/etc/ssh/sshd_config
regexp: ^\s*#?{{ item.option }}\s+.*$
line: "{{ item.option }} {{ item.value }}"
loop:
- { option: LogLevel, value: VERBOSE }
- { option: LoginGraceTime, value: "60" }
- { option: PermitRootLogin, value: "no" }
- { option: StrictModes, value: "yes" }
- { option: MaxAuthTries, value: "4" }
- { option: MaxSessions, value: "10" }
- { option: MaxStartups, value: "10:30:60" }
- { option: PubkeyAuthentication, value: "yes" }
- { option: HostbasedAuthentication, value: "no" }
- { option: IgnoreRhosts, value: "yes" }
- { option: PasswordAuthentication, value: "no" }
- { option: PermitEmptyPasswords, value: "no" }
- { option: KerberosAuthentication, value: "no" }
- { option: GSSAPIAuthentication, value: "no" }
- { option: AllowAgentForwarding, value: "no" }
- { option: AllowTcpForwarding, value: "no" }
- { option: ChallengeResponseAuthentication, value: "no" }
- { option: GatewayPorts, value: "no" }
- { option: X11Forwarding, value: "no" }
- { option: PermitUserEnvironment, value: "no" }
- { option: ClientAliveInterval, value: "300" }
- { option: ClientAliveCountMax, value: "1" }
- { option: PermitTunnel, value: "no" }
- { option: Banner, value: /etc/issue.net }
loop: "{{ cis_cfg.sshd_options }}"
loop_control:
label: "{{ item.option }}"
- name: Detect target OpenSSH version
ansible.builtin.shell: >-
set -o pipefail && {{ chroot_command }} ssh -V 2>&1 | grep -oP 'OpenSSH_\K[0-9]+\.[0-9]+'
args:
executable: /bin/bash
register: cis_sshd_openssh_version
changed_when: false
failed_when: false
- name: Append CIS specific configurations to sshd_config
vars:
cis_sshd_has_mlkem: "{{ (cis_sshd_openssh_version.stdout | default('0.0') is version('9.9', '>=')) }}"
cis_sshd_kex: >-
{{
(['mlkem768x25519-sha256'] if cis_sshd_has_mlkem | bool else [])
+ ['curve25519-sha256@libssh.org', 'ecdh-sha2-nistp521', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp256']
}}
ansible.builtin.blockinfile:
path: /mnt/etc/ssh/sshd_config
marker: "# {mark} CIS SSH HARDENING"
block: |-
## CIS Specific
Protocol 2
### Ciphers and keying ###
RekeyLimit 512M 6h
KexAlgorithms mlkem768x25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256
KexAlgorithms {{ cis_sshd_kex | join(',') }}
Ciphers aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
###########################
AllowStreamLocalForwarding no
PermitUserRC no
AllowUsers *
AllowGroups *
DenyUsers nobody
DenyGroups nobody

View File

@@ -5,26 +5,6 @@
mode: "0644"
content: |
## CIS Sysctl configurations
kernel.yama.ptrace_scope=1
kernel.randomize_va_space=2
# Network
net.ipv4.ip_forward=0
net.ipv4.tcp_syncookies=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.icmp_ignore_bogus_error_responses=1
net.ipv4.conf.all.log_martians=1
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.all.secure_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.log_martians=1
net.ipv4.conf.default.rp_filter=1
net.ipv4.conf.default.secure_redirects=0
net.ipv4.conf.default.send_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv6.conf.all.accept_redirects=0
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.accept_redirects=0
net.ipv6.conf.default.disable_ipv6=1
net.ipv6.conf.lo.disable_ipv6=1
{% for key, value in cis_cfg.sysctl | dictsort %}
{{ key }}={{ value }}
{% endfor %}

21
roles/cis/vars/main.yml Normal file
View File

@@ -0,0 +1,21 @@
---
# OS-specific binary names for CIS permission targets.
# fusermount3 is the modern name; older distros still use fusermount.
cis_fusermount_binary: >-
{{
'fusermount3'
if (
os in ['archlinux', 'fedora', 'rocky', 'rhel']
or (os == 'debian' and (os_version | string) not in ['10', '11'])
or (os == 'almalinux')
)
else 'fusermount'
}}
# write.ul is the Debian 11 name; all others use write.
cis_write_binary: >-
{{
'write.ul'
if (os == 'debian' and (os_version | string) == '11')
else 'write'
}}

View File

@@ -1,9 +1,5 @@
---
cleanup_libvirt_image_dir: >-
{{
system_cfg.path
if system_cfg is defined and (system_cfg.path | string | length) > 0
else '/var/lib/libvirt/images'
}}
cleanup_libvirt_cloudinit_path: >-
{{ [cleanup_libvirt_image_dir, hostname ~ '-cloudinit.iso'] | ansible.builtin.path_join }}
# Post-reboot verification
cleanup_verify_boot: true
cleanup_boot_timeout: 300
cleanup_remove_on_failure: true

View File

@@ -14,19 +14,6 @@
- name: Initialize cleaned VM XML
ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_get_xml.get_xml }}"
changed_when: false
- name: Remove boot ISO device from VM XML (target match)
community.general.xml:
xmlstring: "{{ cleanup_libvirt_domain_xml }}"
xpath: "/domain/devices/disk[target/@dev='sda']"
state: absent
register: cleanup_libvirt_xml_strip_boot
- 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 boot ISO device from VM XML (source match)
when: boot_iso is defined and boot_iso | length > 0
@@ -40,19 +27,17 @@
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 cloud-init ISO device from VM XML (target match)
- name: Remove boot ISO device from VM XML (target fallback)
community.general.xml:
xmlstring: "{{ cleanup_libvirt_domain_xml }}"
xpath: "/domain/devices/disk[target/@dev='sdb']"
xpath: "/domain/devices/disk[target/@dev='sda']"
state: absent
register: cleanup_libvirt_xml_strip_cloudinit
register: cleanup_libvirt_xml_strip_boot
- name: Update cleaned VM XML after removing cloud-init ISO
- name: Update cleaned VM XML after removing boot ISO
ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}"
changed_when: false
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_boot.xmlstring }}"
- name: Remove cloud-init ISO device from VM XML (source match)
community.general.xml:
@@ -64,7 +49,17 @@
- 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:
xmlstring: "{{ cleanup_libvirt_domain_xml }}"
xpath: "/domain/devices/disk[target/@dev='sdb']"
state: absent
register: cleanup_libvirt_xml_strip_cloudinit
- name: Update cleaned VM XML after removing cloud-init ISO
ansible.builtin.set_fact:
cleanup_libvirt_domain_xml: "{{ cleanup_libvirt_xml_strip_cloudinit.xmlstring }}"
- name: Strip XML declaration for libvirt define
ansible.builtin.set_fact:
@@ -76,7 +71,6 @@
| regex_replace("(?i)encoding=[\"'][^\"']+[\"']", "")
| trim
}}
changed_when: false
- name: Update VM definition without installer media
community.libvirt.virt:
@@ -85,19 +79,21 @@
- name: Remove cloud-init disk
ansible.builtin.file:
path: "{{ cleanup_libvirt_cloudinit_path }}"
path: "{{ virtualization_libvirt_cloudinit_path }}"
state: absent
- name: Ensure VM is powered off before restart
community.libvirt.virt:
name: "{{ hostname }}"
state: destroyed
failed_when: false
- name: Start the VM
community.libvirt.virt:
name: "{{ hostname }}"
state: running
# delegate_to inventory_hostname: overrides play-level localhost to run wait_for_connection against the VM
- name: Wait for VM to boot up
delegate_to: "{{ inventory_hostname }}"
ansible.builtin.wait_for_connection:

View File

@@ -3,25 +3,26 @@
when: hypervisor_type == "proxmox"
delegate_to: localhost
become: false
module_defaults:
community.proxmox.proxmox_disk: "{{ _proxmox_auth }}"
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
block:
- name: Cleanup Setup Disks
community.proxmox.proxmox_disk:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
name: "{{ hostname }}"
vmid: "{{ system_cfg.id }}"
disk: "{{ item }}"
state: absent
loop:
- ide0
- ide2
loop: >-
{{
['ide0', 'ide2']
+ (['ide1'] if not (os == 'rhel' and system_cfg.features.rhel_repo.source == 'iso') else [])
}}
failed_when: false
no_log: true
- name: Start the VM
community.proxmox.proxmox_kvm:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
vmid: "{{ system_cfg.id }}"
state: restarted
no_log: true

View File

@@ -6,16 +6,7 @@
ansible.builtin.include_tasks: shutdown.yml
- name: Cleanup hypervisor resources
ansible.builtin.include_tasks: proxmox.yml
- name: Cleanup vCenter resources
ansible.builtin.include_tasks: vmware.yml
- name: Cleanup libvirt resources
ansible.builtin.include_tasks: libvirt.yml
- name: Cleanup Xen resources
ansible.builtin.include_tasks: xen.yml
ansible.builtin.include_tasks: "{{ hypervisor_type }}.yml"
- name: Determine post-reboot connectivity
ansible.builtin.set_fact:
@@ -34,25 +25,27 @@
)
) | bool
}}
changed_when: false
- name: Check VM accessibility after reboot
when:
- cleanup_verify_boot | bool
- system_cfg.type == "virtual"
- cleanup_post_reboot_can_connect | bool
block:
- name: Attempt to connect to VM
delegate_to: "{{ inventory_hostname }}"
ansible.builtin.wait_for_connection:
timeout: 300
timeout: "{{ cleanup_boot_timeout }}"
register: cleanup_vm_connection_check
failed_when: false
changed_when: false
- name: VM failed to boot - initiate cleanup
when:
- cleanup_remove_on_failure | bool
- cleanup_vm_connection_check is defined
- cleanup_vm_connection_check.failed | bool
- virtualization_vm_created_in_run | default(false) | bool
block:
- name: VM boot failure detected - removing VM
ansible.builtin.debug:
@@ -61,147 +54,103 @@
This VM was created in the current playbook run and will be removed
to prevent orphaned resources.
- name: Remove VM for libvirt
when:
- hypervisor_type == "libvirt"
- virtualization_vm_created_in_run | default(false) | bool
- name: Remove failed libvirt VM
when: hypervisor_type == "libvirt"
delegate_to: localhost
become: false
community.libvirt.virt:
name: "{{ hostname }}"
state: destroyed
block:
- name: Destroy libvirt VM
community.libvirt.virt:
name: "{{ hostname }}"
state: destroyed
failed_when: false
- name: Undefine VM for libvirt
when:
- hypervisor_type == "libvirt"
- virtualization_vm_created_in_run | default(false) | bool
delegate_to: localhost
become: false
community.libvirt.virt:
name: "{{ hostname }}"
command: undefine
- name: Undefine libvirt VM
community.libvirt.virt:
name: "{{ hostname }}"
command: undefine
- name: Remove VM disk for libvirt
when:
- hypervisor_type == "libvirt"
- virtualization_vm_created_in_run | default(false) | bool
delegate_to: localhost
become: false
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ virtualization_libvirt_disks | default([]) }}"
loop_control:
label: "{{ item.path }}"
- name: Remove libvirt VM disks
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ virtualization_libvirt_disks | default([]) }}"
loop_control:
label: "{{ item.path }}"
- name: Remove cloud-init disk for libvirt
when:
- hypervisor_type == "libvirt"
- virtualization_vm_created_in_run | default(false) | bool
delegate_to: localhost
become: false
ansible.builtin.file:
path: "{{ virtualization_libvirt_cloudinit_path }}"
state: absent
- name: Remove libvirt cloud-init disk
ansible.builtin.file:
path: "{{ virtualization_libvirt_cloudinit_path }}"
state: absent
- name: Remove VM for proxmox
when:
- hypervisor_type == "proxmox"
- virtualization_vm_created_in_run | default(false) | bool
- name: Remove failed Proxmox VM
when: hypervisor_type == "proxmox"
delegate_to: localhost
become: false
community.proxmox.proxmox_kvm:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
name: "{{ hostname }}"
vmid: "{{ system_cfg.id }}"
state: stopped
module_defaults:
community.proxmox.proxmox_kvm: "{{ _proxmox_auth_node }}"
no_log: true
block:
- name: Stop Proxmox VM
community.proxmox.proxmox_kvm:
name: "{{ hostname }}"
vmid: "{{ system_cfg.id }}"
state: stopped
- name: Delete VM for proxmox
when:
- hypervisor_type == "proxmox"
- virtualization_vm_created_in_run | default(false) | bool
delegate_to: localhost
become: false
community.proxmox.proxmox_kvm:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
name: "{{ hostname }}"
vmid: "{{ system_cfg.id }}"
state: absent
unprivileged: false
- name: Delete Proxmox VM
community.proxmox.proxmox_kvm:
name: "{{ hostname }}"
vmid: "{{ system_cfg.id }}"
state: absent
unprivileged: false
- name: Remove VM for VMware
when:
- hypervisor_type == "vmware"
- virtualization_vm_created_in_run | default(false) | bool
- name: Remove failed VMware VM
when: hypervisor_type == "vmware"
delegate_to: localhost
become: false
community.vmware.vmware_guest:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
name: "{{ hostname }}"
folder: "{{ system_cfg.path | default('/') }}"
state: poweredoff
module_defaults:
community.vmware.vmware_guest: "{{ _vmware_auth }}"
no_log: true
block:
- name: Power off VMware VM
community.vmware.vmware_guest:
name: "{{ hostname }}"
folder: "{{ system_cfg.path | default('/') }}"
state: poweredoff
- name: Delete VM for VMware
when:
- hypervisor_type == "vmware"
- virtualization_vm_created_in_run | default(false) | bool
delegate_to: localhost
become: false
community.vmware.vmware_guest:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
name: "{{ hostname }}"
folder: "{{ system_cfg.path | default('/') }}"
state: absent
- name: Delete VMware VM
community.vmware.vmware_guest:
name: "{{ hostname }}"
folder: "{{ system_cfg.path | default('/') }}"
state: absent
- name: Destroy Xen VM if running
when:
- hypervisor_type == "xen"
- virtualization_vm_created_in_run | default(false) | bool
- name: Remove failed Xen VM
when: hypervisor_type == "xen"
delegate_to: localhost
become: false
ansible.builtin.command:
argv:
- xl
- destroy
- "{{ hostname }}"
register: cleanup_xen_destroy
failed_when: false
changed_when: cleanup_xen_destroy.rc == 0
block:
- name: Destroy Xen VM if running
ansible.builtin.command:
argv:
- xl
- destroy
- "{{ hostname }}"
register: cleanup_xen_destroy
failed_when: false
changed_when: cleanup_xen_destroy.rc == 0
- name: Remove Xen VM disk
when:
- hypervisor_type == "xen"
- virtualization_vm_created_in_run | default(false) | bool
delegate_to: localhost
become: false
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ virtualization_xen_disks | default([]) }}"
loop_control:
label: "{{ item.path }}"
- name: Remove Xen VM disks
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ virtualization_xen_disks | default([]) }}"
loop_control:
label: "{{ item.path }}"
- name: Remove Xen VM config file
when:
- hypervisor_type == "xen"
- virtualization_vm_created_in_run | default(false) | bool
delegate_to: localhost
become: false
ansible.builtin.file:
path: "/tmp/xen-{{ hostname }}.cfg"
state: absent
- name: Remove Xen VM config file
ansible.builtin.file:
path: "/tmp/xen-{{ hostname }}.cfg"
state: absent
- name: VM cleanup completed
ansible.builtin.debug:

View File

@@ -3,36 +3,45 @@
when: hypervisor_type == "vmware"
delegate_to: localhost
become: false
module_defaults:
community.vmware.vmware_guest: "{{ _vmware_auth }}"
vmware.vmware.vm_powerstate: "{{ _vmware_auth }}"
no_log: true
block:
- name: Remove CD-ROM from VM in vCenter
community.vmware.vmware_guest:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
name: "{{ hostname }}"
cdrom:
- controller_number: 0
unit_number: 0
controller_type: sata
type: iso
iso_path: "{{ boot_iso }}"
state: absent
- controller_number: 0
unit_number: 1
controller_type: sata
type: iso
iso_path: "{{ rhel_iso if rhel_iso is defined and rhel_iso | length > 0 else omit }}"
state: absent
cdrom: >-
{{
[
{
'controller_number': 0,
'unit_number': 0,
'controller_type': 'sata',
'type': 'iso',
'iso_path': boot_iso,
'state': 'absent'
}
]
+ (
[
{
'controller_number': 0,
'unit_number': 1,
'controller_type': 'sata',
'type': 'iso',
'iso_path': rhel_iso,
'state': 'absent'
}
]
if (rhel_iso is defined and rhel_iso | length > 0
and not (os == 'rhel' and system_cfg.features.rhel_repo.source == 'iso'))
else []
)
}}
failed_when: false
- name: Start VM in vCenter
vmware.vmware.vm_powerstate:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
name: "{{ hostname }}"
state: powered-on

View File

@@ -3,36 +3,15 @@
when: hypervisor_type == "xen"
delegate_to: localhost
become: false
vars:
xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}"
block:
- name: Ensure Xen disk definitions exist
when: virtualization_xen_disks is not defined
ansible.builtin.set_fact:
cleanup_xen_disks: "{{ cleanup_xen_disks | default([]) + [cleanup_xen_disk_cfg] }}"
vars:
device_letter_map: "abcdefghijklmnopqrstuvwxyz"
device_letter: "{{ device_letter_map[ansible_loop.index0] }}"
cleanup_xen_disk_cfg: >-
{{
{
'path': (
virtualization_xen_disk_path ~ '/' ~ hostname ~ '.qcow2'
if ansible_loop.index0 == 0
else virtualization_xen_disk_path ~ '/' ~ hostname ~ '-disk' ~ ansible_loop.index0 ~ '.qcow2'
),
'target': 'xvd' ~ device_letter,
'size': (item.size | float)
}
}}
loop: "{{ system_cfg.disks }}"
loop_control:
label: "{{ item | to_json }}"
extended: true
changed_when: false
ansible.builtin.include_tasks: ../../virtualization/tasks/_xen_disks.yml
- name: Render Xen VM configuration without installer media
vars:
xen_installer_media_enabled: false
virtualization_xen_disks: "{{ virtualization_xen_disks | default(cleanup_xen_disks | default([])) }}"
ansible.builtin.template:
src: xen.cfg.j2
dest: /tmp/xen-{{ hostname }}.cfg
@@ -56,3 +35,8 @@
- /tmp/xen-{{ hostname }}.cfg
register: cleanup_xen_start_result
changed_when: cleanup_xen_start_result.rc == 0
- name: Remove temporary Xen configuration file
ansible.builtin.file:
path: /tmp/xen-{{ hostname }}.cfg
state: absent

View File

@@ -0,0 +1,7 @@
---
# Network configuration dispatch — maps OS name to the task file
# that writes network config. Default (NetworkManager) applies to
# all OSes not explicitly listed.
configuration_network_task_map:
alpine: network_alpine.yml
void: network_void.yml

View File

@@ -0,0 +1,19 @@
---
# Shared task: update BLS (Boot Loader Specification) entries with kernel cmdline.
# Expects variable: _bls_cmdline (the kernel command line string)
- name: Find BLS entries
ansible.builtin.find:
paths: /mnt/boot/loader/entries
patterns: "*.conf"
register: _bls_entries
changed_when: false
- name: Update BLS options
when: _bls_entries.files | length > 0
ansible.builtin.lineinfile:
path: "{{ item.path }}"
regexp: "^options "
line: "options {{ _bls_cmdline }}"
loop: "{{ _bls_entries.files }}"
loop_control:
label: "{{ item.path }}"

View File

@@ -0,0 +1,16 @@
---
# Resolve platform-specific configuration for the target OS family.
# Sets _configuration_platform from configuration_platform_config[os_family].
- name: Resolve platform-specific configuration
ansible.builtin.assert:
that:
- os_family is defined
- os_family in configuration_platform_config
fail_msg: >-
Unsupported os_family '{{ os_family | default("undefined") }}'.
Extend configuration_platform_config in vars/main.yml.
quiet: true
- name: Set platform configuration
ansible.builtin.set_fact:
_configuration_platform: "{{ configuration_platform_config[os_family] }}"

View File

@@ -23,6 +23,22 @@
- /mnt/etc/motd.d/insights-client
failed_when: false
- name: Create login banner
ansible.builtin.copy:
dest: "{{ item }}"
content: |
**************************************************************
* WARNING: Unauthorized access to this system is prohibited. *
* All activities are monitored and logged. *
* Disconnect immediately if you are not an authorized user. *
**************************************************************
owner: root
group: root
mode: "0644"
loop:
- /mnt/etc/issue
- /mnt/etc/issue.net
- name: Configure sudo banner
when: system_cfg.features.banner.sudo | bool
block:

View File

@@ -1,27 +1,39 @@
---
- name: Configure Bootloader
vars:
_efi_vendor: >-
{{
"redhat" if os == "rhel"
else ("ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os)
}}
_efi_loader: "{{ _configuration_platform.efi_loader }}"
block:
- name: Install Bootloader
vars:
configuration_use_efibootmgr: "{{ is_rhel | bool }}"
configuration_efi_dir: "{{ partitioning_efi_mountpoint }}"
configuration_bootloader_id: >-
{{ "ubuntu" if os in ["ubuntu", "ubuntu-lts"] else os }}
configuration_efi_vendor: >-
{{ "redhat" if os == "rhel" else os }}
configuration_efibootmgr_cmd: >-
/usr/sbin/efibootmgr -c -L '{{ os }}' -d "{{ install_drive }}" -p 1
-l '\efi\EFI\{{ configuration_efi_vendor }}\shimx64.efi'
configuration_grub_cmd: >-
/usr/sbin/grub-install --target=x86_64-efi
--efi-directory={{ configuration_efi_dir }}
--bootloader-id={{ configuration_bootloader_id }}
configuration_bootloader_cmd: >-
{{ configuration_efibootmgr_cmd if configuration_use_efibootmgr else configuration_grub_cmd }}
ansible.builtin.command: "{{ chroot_command }} {{ configuration_bootloader_cmd }}"
- name: Install GRUB EFI binary
when: _configuration_platform.grub_install
ansible.builtin.command: >-
{{ chroot_command }} /usr/sbin/grub-install --target=x86_64-efi
--efi-directory={{ partitioning_efi_mountpoint }}
--bootloader-id={{ _efi_vendor }}
--no-nvram
register: configuration_bootloader_result
changed_when: configuration_bootloader_result.rc == 0
- name: Check existing EFI boot entries
ansible.builtin.command: efibootmgr
register: configuration_efi_entries
changed_when: false
- name: Ensure EFI boot entry exists
when: ('* ' + _efi_vendor) not in configuration_efi_entries.stdout
ansible.builtin.command: >-
efibootmgr -c
-L '{{ _efi_vendor }}'
-d '{{ install_drive }}'
-p 1
-l '\EFI\{{ _efi_vendor }}\{{ _efi_loader }}'
register: configuration_efi_entry_result
changed_when: configuration_efi_entry_result.rc == 0
- name: Ensure lvm2 for non btrfs filesystems
when: os == "archlinux" and system_cfg.filesystem != "btrfs"
ansible.builtin.lineinfile:
@@ -31,33 +43,17 @@
backrefs: true
- name: Regenerate initramfs
when: os not in ["alpine", "void"]
vars:
configuration_initramfs_cmd: >-
{{
'/usr/sbin/mkinitcpio -P'
if os == "archlinux"
else (
'/usr/bin/env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin '
+ '/usr/sbin/update-initramfs -u -k all'
if is_debian | bool
else '/usr/bin/dracut --regenerate-all --force'
)
}}
ansible.builtin.command: "{{ chroot_command }} {{ configuration_initramfs_cmd }}"
when: _configuration_platform.initramfs_cmd | length > 0
ansible.builtin.command: "{{ chroot_command }} {{ _configuration_platform.initramfs_cmd }}"
register: configuration_initramfs_result
changed_when: configuration_initramfs_result.rc == 0
- name: Generate grub config
vars:
configuration_efi_vendor: >-
{{ "redhat" if os == "rhel" else os }}
configuration_grub_cfg_cmd: >-
{{
'/usr/sbin/grub2-mkconfig -o '
+ partitioning_efi_mountpoint
+ '/EFI/' + configuration_efi_vendor + '/grub.cfg'
if is_rhel | bool
'/usr/sbin/' + _configuration_platform.grub_mkconfig_prefix + ' -o /boot/grub2/grub.cfg'
if os_family == 'RedHat'
else '/usr/sbin/grub-mkconfig -o /boot/grub/grub.cfg'
}}
ansible.builtin.command: "{{ chroot_command }} {{ configuration_grub_cfg_cmd }}"

View File

@@ -1,6 +1,7 @@
---
- name: Configure disk encryption
when: system_cfg.luks.enabled | bool
no_log: true
vars:
configuration_luks_passphrase: >-
{{ system_cfg.luks.passphrase | string }}
@@ -35,7 +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"
changed_when: false
- name: Validate LUKS UUID is available
ansible.builtin.assert:
@@ -59,6 +59,14 @@
when: configuration_luks_auto_method == 'keyfile'
ansible.builtin.include_tasks: encryption/keyfile.yml
- name: Record final LUKS auto-decrypt method
ansible.builtin.set_fact:
configuration_luks_final_method: "{{ configuration_luks_auto_method }}"
- name: Report LUKS auto-decrypt configuration
ansible.builtin.debug:
msg: "LUKS auto-decrypt method: {{ configuration_luks_final_method }}"
- name: Build LUKS parameters
vars:
luks_keyfile_in_use: "{{ configuration_luks_auto_method == 'keyfile' }}"
@@ -114,231 +122,16 @@
path: /mnt{{ configuration_luks_keyfile_path }}
state: absent
- 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:
- is_debian | bool
- 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 initramfs
ansible.builtin.include_tasks: encryption/initramfs.yml
- 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 filesystems fsck)
- name: Configure dracut
when: os_family == 'RedHat'
ansible.builtin.include_tasks: encryption/dracut.yml
- 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: is_rhel | bool
ansible.builtin.file:
path: /mnt/etc/dracut.conf.d
state: directory
mode: "0755"
- name: Configure dracut for LUKS
when: is_rhel | bool
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: Read kernel cmdline defaults
when: is_rhel | bool
ansible.builtin.slurp:
src: /mnt/etc/kernel/cmdline
register: configuration_kernel_cmdline_slurp
- name: Build kernel cmdline with LUKS args
when: is_rhel | bool
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 }}"
changed_when: false
- name: Write kernel cmdline with LUKS args
when: is_rhel | bool
ansible.builtin.copy:
dest: /mnt/etc/kernel/cmdline
mode: "0644"
content: "{{ configuration_kernel_cmdline_new }}\n"
- name: Find BLS entries
when: is_rhel | bool
ansible.builtin.find:
paths: /mnt/boot/loader/entries
patterns: "*.conf"
register: configuration_kernel_bls_entries
changed_when: false
- name: Update BLS options with LUKS args
when:
- is_rhel | bool
- configuration_kernel_bls_entries.files | length > 0
ansible.builtin.lineinfile:
path: "{{ item.path }}"
regexp: "^options "
line: "options {{ configuration_kernel_cmdline_new }}"
loop: "{{ configuration_kernel_bls_entries.files }}"
loop_control:
label: "{{ item.path }}"
- name: Read grub defaults
when: not is_rhel | bool
ansible.builtin.slurp:
src: /mnt/etc/default/grub
register: configuration_grub_slurp
- name: Build grub command lines with LUKS args
when: not is_rhel | bool
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 is_rhel | bool
ansible.builtin.lineinfile:
path: /mnt/etc/default/grub
regexp: "^GRUB_CMDLINE_LINUX_DEFAULT="
line: 'GRUB_CMDLINE_LINUX_DEFAULT="{{ configuration_grub_cmdline_default_new }}"'
- name: Configure GRUB for LUKS
when: not os_family == 'RedHat'
ansible.builtin.include_tasks: encryption/grub.yml

View File

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

@@ -0,0 +1,56 @@
---
- 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 "
{% if configuration_luks_keyfile_in_use %}
install_items+=" {{ configuration_luks_keyfile_path }} "
{% endif %}
mode: "0644"
- name: Read kernel cmdline defaults
ansible.builtin.slurp:
src: /mnt/etc/kernel/cmdline
register: configuration_kernel_cmdline_slurp
- name: Build kernel cmdline with LUKS args
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
ansible.builtin.copy:
dest: /mnt/etc/kernel/cmdline
mode: "0644"
content: "{{ configuration_kernel_cmdline_new }}\n"
- name: Update BLS entries with LUKS kernel cmdline
vars:
_bls_cmdline: "{{ configuration_kernel_cmdline_new }}"
ansible.builtin.include_tasks: ../_bls_update.yml

View File

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

@@ -0,0 +1,65 @@
---
- 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(' ')
}})

View File

@@ -86,7 +86,6 @@
device: "{{ configuration_luks_device }}"
passphrase: "{{ configuration_luks_passphrase }}"
new_keyfile: "/mnt{{ configuration_luks_keyfile_path }}"
register: configuration_luks_addkey_retry
failed_when: false
no_log: true
@@ -104,6 +103,13 @@
failed_when: false
no_log: true
- name: Warn about keyfile enrollment failure
when: (configuration_luks_keyfile_unlock_test_after.rc | default(1)) != 0
ansible.builtin.debug:
msg: >-
LUKS keyfile enrollment failed — falling back to manual unlock at boot.
The system will prompt for the LUKS passphrase during startup.
- name: Fallback to manual LUKS unlock if keyfile enrollment failed
when: (configuration_luks_keyfile_unlock_test_after.rc | default(1)) != 0
ansible.builtin.set_fact:

View File

@@ -1,6 +1,7 @@
---
- name: Enroll TPM2 for LUKS
block:
# Tempfile in chroot /tmp — accessible by both chroot and host commands
- name: Create temporary passphrase file for TPM2 enrollment
ansible.builtin.tempfile:
path: /mnt/tmp
@@ -78,6 +79,12 @@
chroot stderr={{ configuration_luks_tpm2_enroll_chroot.stderr | default('') }},
host stderr={{ configuration_luks_tpm2_enroll_host.stderr | default('') }}
rescue:
- name: Warn about TPM2 enrollment failure
ansible.builtin.debug:
msg: >-
WARNING: TPM2 enrollment failed — falling back to keyfile auto-decrypt.
The system will use a keyfile instead of TPM2 for automatic LUKS unlock.
- name: Fallback to keyfile auto-decrypt
ansible.builtin.set_fact:
configuration_luks_auto_method: keyfile
@@ -87,4 +94,3 @@
ansible.builtin.file:
path: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}"
state: absent
changed_when: false

View File

@@ -1,7 +1,7 @@
---
- name: Append vim configurations to vimrc
ansible.builtin.blockinfile:
path: "{{ '/mnt/etc/vim/vimrc' if is_debian | bool else '/mnt/etc/vimrc' }}"
path: "{{ '/mnt/etc/vim/vimrc' if os_family == 'Debian' else '/mnt/etc/vimrc' }}"
block: |
set encoding=utf-8
set number
@@ -9,9 +9,11 @@
set smartindent
set mouse=a
insertafter: EOF
marker: ""
marker: "\" {mark} CUSTOM VIM CONFIG"
failed_when: false
# Tuned for VM workloads: low swappiness, aggressive writeback, large page-cluster
# for zram. Override post-bootstrap via the linux role or sysctl if needed.
- name: Add memory tuning parameters
ansible.builtin.blockinfile:
path: /mnt/etc/sysctl.d/90-memory.conf
@@ -22,7 +24,7 @@
vm.dirty_background_ratio=1
vm.dirty_ratio=10
vm.page-cluster=10
marker: ""
marker: "# {mark} MEMORY TUNING"
mode: "0644"
- name: Create zram config
@@ -41,32 +43,7 @@
mode: "0644"
- name: Copy Custom Shell config
ansible.builtin.template:
src: custom.sh.j2
ansible.builtin.copy:
src: custom.sh
dest: /mnt/etc/profile.d/custom.sh
mode: "0644"
- name: Create login banner
ansible.builtin.copy:
dest: "{{ item }}"
content: |
**************************************************************
* WARNING: Unauthorized access to this system is prohibited. *
* All activities are monitored and logged. *
* Disconnect immediately if you are not an authorized user. *
**************************************************************
owner: root
group: root
mode: "0644"
loop:
- /mnt/etc/issue
- /mnt/etc/issue.net
- name: Remove motd files
when: os == "rhel"
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /mnt/etc/motd.d/cockpit
- /mnt/etc/motd.d/insights-client

View File

@@ -23,8 +23,19 @@
regexp: "(xfs.*?)(attr2)"
replace: "\\1allocsize=64m"
- name: Remove RHEL ISO fstab entry when not using local repo
when:
- os == "rhel"
- system_cfg.features.rhel_repo.source != "iso"
ansible.builtin.lineinfile:
path: /mnt/etc/fstab
regexp: "^.*\\/dvd.*$"
state: absent
- name: Replace ISO UUID entry with /dev/sr0 in fstab
when: os == "rhel"
when:
- os == "rhel"
- system_cfg.features.rhel_repo.source == "iso"
vars:
configuration_fstab_dvd_line: >-
{{
@@ -39,7 +50,10 @@
state: present
- name: Write image from RHEL ISO to the target machine
when: os == "rhel" and hypervisor_type == 'vmware'
when:
- os == "rhel"
- hypervisor_type == "vmware"
- system_cfg.features.rhel_repo.source == "iso"
ansible.builtin.command:
argv:
- dd
@@ -63,3 +77,4 @@
- { regexp: "^tmpfs\\s+/dev/shm\\s+", line: "tmpfs /dev/shm tmpfs defaults,nosuid,nodev,noexec 0 0" }
loop_control:
loop_var: fstab_entry
label: "{{ fstab_entry.regexp }}"

View File

@@ -1,6 +1,6 @@
---
- name: Configure grub defaults
when: not is_rhel | bool
when: os_family != 'RedHat'
ansible.builtin.lineinfile:
dest: /mnt/etc/default/grub
regexp: "{{ item.regexp }}"
@@ -10,9 +10,11 @@
line: GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3"
- regexp: ^GRUB_TIMEOUT=
line: GRUB_TIMEOUT=1
loop_control:
label: "{{ item.line }}"
- name: Ensure grub defaults file exists for RHEL-based systems
when: is_rhel | bool
when: os_family == 'RedHat'
block:
- name: Build RHEL kernel command line defaults
vars:
@@ -60,7 +62,6 @@
ansible.builtin.set_fact:
configuration_grub_cmdline_linux_base: "{{ grub_cmdline_linux_base }}"
configuration_kernel_cmdline_base: "{{ grub_kernel_cmdline_base }}"
changed_when: false
- name: Check if grub defaults file exists
ansible.builtin.stat:
@@ -95,22 +96,10 @@
mode: "0644"
content: "{{ configuration_kernel_cmdline_base }}\n"
- name: Find BLS entries
ansible.builtin.find:
paths: /mnt/boot/loader/entries
patterns: "*.conf"
register: configuration_grub_bls_entries
changed_when: false
- name: Update BLS options with kernel cmdline defaults
when: configuration_grub_bls_entries.files | length > 0
ansible.builtin.lineinfile:
path: "{{ item.path }}"
regexp: "^options "
line: "options {{ configuration_kernel_cmdline_base }}"
loop: "{{ configuration_grub_bls_entries.files }}"
loop_control:
label: "{{ item.path }}"
- name: Update BLS entries with kernel cmdline defaults
vars:
_bls_cmdline: "{{ configuration_kernel_cmdline_base }}"
ansible.builtin.include_tasks: _bls_update.yml
- name: Enable GRUB cryptodisk for encrypted /boot
when: partitioning_grub_enable_cryptodisk | bool

View File

@@ -14,16 +14,18 @@
- name: Setup locales
block:
- name: Configure locale.gen
when: not is_rhel | bool
when: _configuration_platform.locale_gen
ansible.builtin.lineinfile:
dest: /mnt/etc/locale.gen
regexp: "{{ item.regex }}"
line: "{{ item.line }}"
loop:
- { regex: "{{ system_cfg.locale }} UTF-8", line: "{{ system_cfg.locale }} UTF-8" }
loop_control:
label: "{{ item.line }}"
- name: Generate locales
when: not is_rhel | bool
when: _configuration_platform.locale_gen
ansible.builtin.command: "{{ chroot_command }} /usr/sbin/locale-gen"
register: configuration_locale_result
changed_when: configuration_locale_result.rc == 0
@@ -45,7 +47,7 @@
- name: Set hostname
ansible.builtin.copy:
content: "{{ configuration_hostname_fqdn }}"
content: "{{ configuration_hostname_fqdn.split('.')[0] }}"
dest: /mnt/etc/hostname
mode: "0644"

View File

@@ -1,19 +1,28 @@
---
- name: Resolve platform configuration
ansible.builtin.import_tasks: _resolve_platform.yml
- name: Include configuration tasks
ansible.builtin.include_tasks: "{{ configuration_task }}"
when: configuration_task.when | default(true)
ansible.builtin.include_tasks: "{{ configuration_task.file }}"
loop:
- banner.yml
- fstab.yml
- locales.yml
- ssh.yml
- services.yml
- grub.yml
- encryption.yml
- bootloader.yml
- extras.yml
- network.yml
- users.yml
- sudo.yml
- selinux.yml
- file: repositories.yml
when: "{{ os_family == 'Debian' }}"
- file: banner.yml
- file: fstab.yml
- file: locales.yml
- file: ssh.yml
- file: services.yml
- file: grub.yml
- file: encryption.yml
when: "{{ system_cfg.luks.enabled | bool }}"
- file: bootloader.yml
- file: extras.yml
- file: network.yml
- file: users.yml
- file: sudo.yml
- file: selinux.yml
when: "{{ os_family == 'RedHat' }}"
loop_control:
loop_var: configuration_task
label: "{{ configuration_task.file }}"

View File

@@ -29,88 +29,10 @@
- configuration_detected_interfaces | length > 0
fail_msg: Failed to detect any network interfaces.
- name: Configure NetworkManager profiles
when: os not in ["alpine", "void"]
block:
- name: Copy NetworkManager keyfile per interface
vars:
configuration_iface: "{{ item }}"
configuration_iface_name: "{{ configuration_detected_interfaces[idx] | default('eth' ~ idx) }}"
configuration_net_uuid: "{{ ('LAN-' ~ idx ~ '-' ~ hostname) | ansible.builtin.to_uuid }}"
ansible.builtin.template:
src: network.j2
dest: "/mnt/etc/NetworkManager/system-connections/LAN-{{ idx }}.nmconnection"
mode: "0600"
loop: "{{ system_cfg.network.interfaces }}"
loop_control:
index_var: idx
label: "LAN-{{ idx }}"
- name: Set DNS configuration facts
ansible.builtin.set_fact:
configuration_dns_list: "{{ system_cfg.network.dns.servers }}"
configuration_dns_search: "{{ system_cfg.network.dns.search }}"
- name: Fix Ubuntu unmanaged devices
when: os in ["ubuntu", "ubuntu-lts"]
ansible.builtin.file:
path: /mnt/etc/NetworkManager/conf.d/10-globally-managed-devices.conf
state: touch
mode: "0644"
- name: Configure Alpine networking
when: os == "alpine"
vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
block:
- name: Write Alpine network interfaces
ansible.builtin.copy:
dest: /mnt/etc/network/interfaces
mode: "0644"
content: |
auto lo
iface lo inet loopback
{% for iface in system_cfg.network.interfaces %}
{% set iface_name = configuration_detected_interfaces[loop.index0] | default(iface.name | default('eth' ~ loop.index0)) %}
{% set has_static = (iface.ip | default('') | string | length) > 0 %}
auto {{ iface_name }}
iface {{ iface_name }} inet {{ 'static' if has_static else 'dhcp' }}
{% if has_static %}
address {{ iface.ip }}/{{ iface.prefix }}
{% if iface.gateway | default('') | string | length %}
gateway {{ iface.gateway }}
{% endif %}
{% endif %}
{% endfor %}
- name: Set Alpine DNS resolvers
when: configuration_dns_list | length > 0
ansible.builtin.copy:
dest: /mnt/etc/resolv.conf
mode: "0644"
content: |
{% for resolver in configuration_dns_list %}
nameserver {{ resolver }}
{% endfor %}
- name: Configure Void networking
when: os == "void"
vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
block:
- name: Write dhcpcd configuration
ansible.builtin.copy:
dest: /mnt/etc/dhcpcd.conf
mode: "0644"
content: |
{% for iface in system_cfg.network.interfaces %}
{% set iface_name = configuration_detected_interfaces[loop.index0] | default(iface.name | default('eth' ~ loop.index0)) %}
{% set has_static = (iface.ip | default('') | string | length) > 0 %}
{% if has_static %}
interface {{ iface_name }}
static ip_address={{ iface.ip }}/{{ iface.prefix }}
{% if iface.gateway | default('') | string | length %}
static routers={{ iface.gateway }}
{% endif %}
{% if loop.index0 == 0 and configuration_dns_list | length > 0 %}
static domain_name_servers={{ configuration_dns_list | join(' ') }}
{% endif %}
{% endif %}
{% endfor %}
- name: Configure networking
ansible.builtin.include_tasks: "{{ configuration_network_task_map[os] | default('network_nm.yml') }}"

View File

@@ -0,0 +1,36 @@
---
- name: Write Alpine network interfaces
ansible.builtin.copy:
dest: /mnt/etc/network/interfaces
mode: "0644"
content: |
auto lo
iface lo inet loopback
{% for iface in system_cfg.network.interfaces %}
{% set inv_name = iface.name | default('') | string %}
{% set det_name = configuration_detected_interfaces[loop.index0] | default('eth' ~ loop.index0) %}
{% set iface_name = inv_name if inv_name | length > 0 else det_name %}
{% set has_static = (iface.ip | default('') | string | length) > 0 %}
auto {{ iface_name }}
iface {{ iface_name }} inet {{ 'static' if has_static else 'dhcp' }}
{% if has_static %}
address {{ iface.ip }}/{{ iface.prefix }}
{% if iface.gateway | default('') | string | length %}
gateway {{ iface.gateway }}
{% endif %}
{% endif %}
{% endfor %}
- name: Set Alpine DNS resolvers
when: configuration_dns_list | length > 0 or configuration_dns_search | length > 0
ansible.builtin.copy:
dest: /mnt/etc/resolv.conf
mode: "0644"
content: |
{% if configuration_dns_search | length > 0 %}
search {{ configuration_dns_search | join(' ') }}
{% endif %}
{% for resolver in configuration_dns_list %}
nameserver {{ resolver }}
{% endfor %}

View File

@@ -0,0 +1,21 @@
---
- name: Copy NetworkManager keyfile per interface
vars:
configuration_iface: "{{ item }}"
configuration_iface_name: "{{ item.name | default(configuration_detected_interfaces[idx] | default('')) }}"
configuration_net_uuid: "{{ ('LAN-' ~ idx ~ '-' ~ hostname) | ansible.builtin.to_uuid }}"
ansible.builtin.template:
src: network.j2
dest: "/mnt/etc/NetworkManager/system-connections/LAN-{{ idx }}.nmconnection"
mode: "0600"
loop: "{{ system_cfg.network.interfaces }}"
loop_control:
index_var: idx
label: "LAN-{{ idx }}"
- name: Fix Ubuntu unmanaged devices
when: os in ["ubuntu", "ubuntu-lts"]
ansible.builtin.file:
path: /mnt/etc/NetworkManager/conf.d/10-globally-managed-devices.conf
state: touch
mode: "0644"

View File

@@ -0,0 +1,26 @@
---
- name: Write dhcpcd configuration
ansible.builtin.copy:
dest: /mnt/etc/dhcpcd.conf
mode: "0644"
content: |
{% for iface in system_cfg.network.interfaces %}
{% set inv_name = iface.name | default('') | string %}
{% set det_name = configuration_detected_interfaces[loop.index0] | default('eth' ~ loop.index0) %}
{% set iface_name = inv_name if inv_name | length > 0 else det_name %}
{% set has_static = (iface.ip | default('') | string | length) > 0 %}
{% if has_static %}
interface {{ iface_name }}
static ip_address={{ iface.ip }}/{{ iface.prefix }}
{% if iface.gateway | default('') | string | length %}
static routers={{ iface.gateway }}
{% endif %}
{% if loop.index0 == 0 and configuration_dns_list | length > 0 %}
static domain_name_servers={{ configuration_dns_list | join(' ') }}
{% endif %}
{% if loop.index0 == 0 and configuration_dns_search | length > 0 %}
static domain_search={{ configuration_dns_search | join(' ') }}
{% endif %}
{% endif %}
{% endfor %}

View File

@@ -0,0 +1,25 @@
---
- name: Write final sources.list
vars:
_debian_release_map:
"10": buster
"11": bullseye
"12": bookworm
"13": trixie
unstable: sid
_ubuntu_release_map:
ubuntu: questing
ubuntu-lts: noble
ansible.builtin.template:
src: "{{ os | replace('-lts', '') }}.sources.list.j2"
dest: /mnt/etc/apt/sources.list
mode: "0644"
- name: Ensure apt performance configuration persists
ansible.builtin.copy:
dest: /mnt/etc/apt/apt.conf.d/99performance
content: |
Acquire::Retries "3";
Acquire::http::Pipeline-Depth "10";
APT::Install-Recommends "false";
mode: "0644"

View File

@@ -1,6 +1,6 @@
---
- name: Fix SELinux
when: is_rhel | bool
when: os_family == 'RedHat'
block:
- name: Fix SELinux by pre-labeling the filesystem before first boot
when: os in ['almalinux', 'rocky', 'rhel'] and system_cfg.features.selinux.enabled | bool
@@ -11,6 +11,8 @@
register: configuration_setfiles_result
changed_when: configuration_setfiles_result.rc == 0
# Fedora: setfiles segfaults during bootstrap chroot relabeling, so SELinux
# is left permissive and expected to relabel on first boot.
- name: Disable SELinux
when: os == "fedora" or not system_cfg.features.selinux.enabled | bool
ansible.builtin.lineinfile:

View File

@@ -1,23 +1,22 @@
---
- name: Enable Systemd Services
when: os not in ['alpine', 'void']
ansible.builtin.command: >
{{ chroot_command }} systemctl enable NetworkManager
{{ ' firewalld' if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else '' }}
{{ ' ufw' if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else '' }}
{{
(' ssh' if is_debian | bool else ' sshd')
if system_cfg.features.ssh.enabled | bool else ''
}}
{{
'logrotate systemd-resolved systemd-timesyncd systemd-networkd'
if os == 'archlinux' else ''
}}
register: configuration_enable_services_result
changed_when: configuration_enable_services_result.rc == 0
- name: Enable systemd services
when: _configuration_platform.init_system == 'systemd'
vars:
configuration_systemd_services: >-
{{
['NetworkManager']
+ (['firewalld'] if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else [])
+ (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else [])
+ ([_configuration_platform.ssh_service] if system_cfg.features.ssh.enabled | bool else [])
+ (['logrotate', 'systemd-timesyncd'] if os == 'archlinux' 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: Enable OpenRC services
when: os == 'alpine'
when: _configuration_platform.init_system == 'openrc'
vars:
configuration_openrc_services: >-
{{
@@ -37,7 +36,6 @@
path: "/mnt/etc/init.d/{{ item }}"
loop: "{{ configuration_openrc_services }}"
register: configuration_openrc_service_stats
changed_when: false
- name: Enable OpenRC services
ansible.builtin.file:
@@ -45,10 +43,12 @@
dest: "/mnt/etc/runlevels/default/{{ item.item }}"
state: link
loop: "{{ configuration_openrc_service_stats.results }}"
loop_control:
label: "{{ item.item }}"
when: item.stat.exists
- name: Enable runit services
when: os == 'void'
when: _configuration_platform.init_system == 'runit'
vars:
configuration_runit_services: >-
{{
@@ -68,7 +68,6 @@
path: "/mnt/etc/sv/{{ item }}"
loop: "{{ configuration_runit_services }}"
register: configuration_runit_service_stats
changed_when: false
- name: Enable runit services
ansible.builtin.file:
@@ -76,4 +75,6 @@
dest: "/mnt/var/service/{{ item.item }}"
state: link
loop: "{{ configuration_runit_service_stats.results }}"
loop_control:
label: "{{ item.item }}"
when: item.stat.exists

View File

@@ -1,4 +1,6 @@
---
# Bootstrap-only: permissive SSH for initial Ansible access.
# Post-bootstrap hardening (key-only, no root login) is handled by the linux role.
- name: Ensure SSH password authentication is enabled
ansible.builtin.lineinfile:
path: /mnt/etc/ssh/sshd_config

View File

@@ -9,18 +9,21 @@
- name: Give sudo access to wheel group
ansible.builtin.copy:
content: "{{ '%sudo ALL=(ALL) ALL\n' if is_debian | bool else '%wheel ALL=(ALL) ALL\n' }}"
content: "{{ _configuration_platform.sudo_group }} ALL=(ALL) ALL\n"
dest: /mnt/etc/sudoers.d/01-wheel
mode: "0440"
validate: /usr/sbin/visudo --check --file=%s
- name: Deploy per-user sudoers rules
when: item.sudo is defined and (item.sudo | string | length) > 0
when: item.value.sudo is defined and (item.value.sudo | string | length > 0)
vars:
configuration_sudoers_rule: >-
{{ item.value.sudo if item.value.sudo is string else 'ALL=(ALL) NOPASSWD: ALL' }}
ansible.builtin.copy:
content: "{{ item.name }} {{ item.sudo }}\n"
dest: "/mnt/etc/sudoers.d/{{ item.name }}"
content: "{{ item.key }} {{ configuration_sudoers_rule }}\n"
dest: "/mnt/etc/sudoers.d/{{ item.key }}"
mode: "0440"
validate: /usr/sbin/visudo --check --file=%s
loop: "{{ system_cfg.users }}"
loop: "{{ system_cfg.users | dict2items }}"
loop_control:
label: "{{ item.name }}"
label: "{{ item.key }}"

View File

@@ -1,53 +1,68 @@
---
- name: Set root password
vars:
configuration_root_cmd: >-
{{ chroot_command }} /usr/sbin/usermod --password
'{{ system_cfg.root.password | password_hash('sha512') }}' root --shell /bin/bash
ansible.builtin.command: "{{ configuration_root_cmd }}"
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
args:
executable: /bin/bash
register: configuration_root_result
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
register: configuration_root_shell_result
changed_when: configuration_root_shell_result.rc == 0
- name: Create user accounts
vars:
configuration_user_group: >-
{{ "sudo" if is_debian | bool else "wheel" }}
configuration_user_group: "{{ _configuration_platform.user_group }}"
configuration_useradd_cmd: >-
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group
--uid {{ 1000 + ansible_loop.index0 }}
--groups {{ configuration_user_group }} {{ item.name }}
--password {{ item.password | password_hash('sha512') }} --shell /bin/bash
--uid {{ 1000 + _idx }}
--groups {{ configuration_user_group }} {{ item.key }}
{{ ('--password ' ~ (item.value.password | password_hash('sha512'))) if (item.value.password | default('') | string | length > 0) else '' }}
--shell {{ item.value.shell | default('/bin/bash') }}
ansible.builtin.command: "{{ configuration_useradd_cmd }}"
loop: "{{ system_cfg.users }}"
loop: "{{ system_cfg.users | dict2items }}"
loop_control:
extended: true
label: "{{ item.name }}"
index_var: _idx
label: "{{ item.key }}"
register: configuration_user_result
changed_when: configuration_user_result.rc == 0
no_log: true
- name: Ensure .ssh directory exists
when: item.keys | default([]) | length > 0
when: (item.value['keys'] | default([]) | length) > 0
ansible.builtin.file:
path: "/mnt/home/{{ item.name }}/.ssh"
path: "/mnt/home/{{ item.key }}/.ssh"
state: directory
owner: "{{ 1000 + ansible_loop.index0 }}"
group: "{{ 1000 + ansible_loop.index0 }}"
owner: "{{ 1000 + _idx }}"
group: "{{ 1000 + _idx }}"
mode: "0700"
loop: "{{ system_cfg.users }}"
loop: "{{ system_cfg.users | dict2items }}"
loop_control:
extended: true
label: "{{ item.name }}"
index_var: _idx
label: "{{ item.key }}"
- name: Add SSH public keys to authorized_keys
vars:
_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: "{{ _uid }}"
group: "{{ _uid }}"
- 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 }}"
mode: "0600"
create: true
loop: "{{ system_cfg.users | subelements('keys', skip_missing=True) }}"
loop: "{{ system_cfg.users | dict2items }}"
loop_control:
label: "{{ item.0.name }}: {{ item.1[:40] }}..."
index_var: _idx
label: "{{ item.key }}"

View File

@@ -0,0 +1,15 @@
# Managed by Ansible.
{% set release = _debian_release_map[os_version | string] | default('trixie') %}
{% set mirror = system_cfg.mirror %}
{% set components = 'main contrib non-free' ~ (' non-free-firmware' if (os_version | string) not in ['10', '11'] else '') %}
deb {{ mirror }} {{ release }} {{ components }}
deb-src {{ mirror }} {{ release }} {{ components }}
{% if release != 'sid' %}
deb https://security.debian.org/debian-security {{ release }}-security {{ components }}
deb-src https://security.debian.org/debian-security {{ release }}-security {{ components }}
deb {{ mirror }} {{ release }}-updates {{ components }}
deb-src {{ mirror }} {{ release }}-updates {{ components }}
{% endif %}

View File

@@ -2,12 +2,15 @@
id=LAN-{{ idx }}
uuid={{ configuration_net_uuid }}
type=ethernet
autoconnect-priority=10
{% if configuration_iface_name | length > 0 %}
interface-name={{ configuration_iface_name }}
{% endif %}
[ipv4]
{% set iface = configuration_iface %}
{% set dns_list = system_cfg.network.dns.servers | default([]) %}
{% set search_list = system_cfg.network.dns.search | default([]) %}
{% set dns_list = configuration_dns_list %}
{% set search_list = configuration_dns_search %}
{% if iface.ip | default('') | string | length %}
address1={{ iface.ip }}/{{ iface.prefix }}{{ (',' ~ iface.gateway) if (iface.gateway | default('') | string | length) else '' }}
method=manual
@@ -15,11 +18,11 @@ method=manual
method=auto
{% endif %}
{% if idx | int == 0 and dns_list %}
dns={{ dns_list | join(';') }}
dns={{ dns_list | join(';') }};
ignore-auto-dns=true
{% endif %}
{% if idx | int == 0 and search_list %}
dns-search={{ search_list | join(';') }}
dns-search={{ search_list | join(';') }};
{% endif %}
[ipv6]

View File

@@ -0,0 +1,16 @@
# Managed by Ansible.
{% set release = _ubuntu_release_map[os] | default('noble') %}
{% set mirror = system_cfg.mirror %}
{% set components = 'main restricted universe multiverse' %}
deb {{ mirror }} {{ release }} {{ components }}
deb-src {{ mirror }} {{ release }} {{ components }}
deb {{ mirror }} {{ release }}-updates {{ components }}
deb-src {{ mirror }} {{ release }}-updates {{ components }}
deb {{ mirror }} {{ release }}-security {{ components }}
deb-src {{ mirror }} {{ release }}-security {{ components }}
deb {{ mirror }} {{ release }}-backports {{ components }}
deb-src {{ mirror }} {{ release }}-backports {{ components }}

View File

@@ -0,0 +1,67 @@
---
# Platform-specific configuration values keyed by os_family.
# Consumed as _configuration_platform in tasks via:
# configuration_platform_config[os_family]
configuration_platform_config:
RedHat:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: shimx64.efi
grub_install: false
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
grub_mkconfig_prefix: grub2-mkconfig
locale_gen: false
init_system: systemd
Debian:
user_group: sudo
sudo_group: "%sudo"
ssh_service: ssh
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: >-
/usr/bin/env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
/usr/sbin/update-initramfs -u -k all
grub_mkconfig_prefix: grub-mkconfig
locale_gen: true
init_system: systemd
Archlinux:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: "/usr/sbin/mkinitcpio -P"
grub_mkconfig_prefix: grub-mkconfig
locale_gen: true
init_system: systemd
Suse:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: "/usr/bin/dracut --regenerate-all --force"
grub_mkconfig_prefix: grub-mkconfig
locale_gen: true
init_system: systemd
Alpine:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: ""
grub_mkconfig_prefix: grub-mkconfig
locale_gen: false
init_system: openrc
Void:
user_group: wheel
sudo_group: "%wheel"
ssh_service: sshd
efi_loader: grubx64.efi
grub_install: true
initramfs_cmd: ""
grub_mkconfig_prefix: grub-mkconfig
locale_gen: false
init_system: runit

View File

@@ -0,0 +1,10 @@
---
# Connection and timing
environment_wait_timeout: 180
environment_wait_delay: 5
# Pacman installer settings
environment_parallel_downloads: 20
environment_pacman_lock_timeout: 120
environment_pacman_retries: 4
environment_pacman_retry_delay: 15

View File

@@ -0,0 +1,102 @@
---
- name: Select primary Network Interface
when: hypervisor_type == "vmware"
ansible.builtin.set_fact:
environment_interface_name: >-
{{
(
(ansible_facts.interfaces | default(ansible_facts['ansible_interfaces'] | default([])))
| reject('equalto', 'lo')
| list
| first
)
| default('')
}}
- name: Bring up network interface
when:
- hypervisor_type == "vmware"
- environment_interface_name | default('') | length > 0
ansible.builtin.command: "ip link set {{ environment_interface_name }} up"
register: environment_link_result
changed_when: environment_link_result.rc == 0
- name: Set IP-Address
when:
- hypervisor_type == "vmware"
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
ansible.builtin.command: >-
ip addr replace {{ system_cfg.network.ip }}/{{ system_cfg.network.prefix }}
dev {{ environment_interface_name }}
register: environment_ip_result
changed_when: environment_ip_result.rc == 0
- name: Set Default Gateway
when:
- hypervisor_type == "vmware"
- system_cfg.network.gateway is defined and system_cfg.network.gateway | string | length > 0
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
ansible.builtin.command: "ip route replace default via {{ system_cfg.network.gateway }}"
register: environment_gateway_result
changed_when: environment_gateway_result.rc == 0
- name: Configure DNS resolvers
when:
- hypervisor_type == "vmware"
- system_cfg.network.dns.servers | default([]) | length > 0
ansible.builtin.copy:
dest: /etc/resolv.conf
content: |
{% for server in system_cfg.network.dns.servers %}
nameserver {{ server }}
{% endfor %}
{% if system_cfg.network.dns.search | default([]) | length > 0 %}
search {{ system_cfg.network.dns.search | join(' ') }}
{% endif %}
mode: "0644"
- name: Synchronize clock via NTP
ansible.builtin.command: timedatectl set-ntp true
register: environment_ntp_result
changed_when: environment_ntp_result.rc == 0
- name: Configure SSH for root login
when:
- hypervisor_type == "vmware"
- hypervisor_cfg.ssh | default(false) | bool
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
block:
- name: Allow login
ansible.builtin.replace:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
replace: "{{ item.replace }}"
loop:
- regexp: "^#?PermitEmptyPasswords.*"
replace: "PermitEmptyPasswords yes"
- regexp: "^#?PermitRootLogin.*"
replace: "PermitRootLogin yes"
loop_control:
label: "{{ item.replace }}"
- name: Reload SSH service to apply changes
ansible.builtin.service:
name: sshd
state: reloaded
- 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_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
- name: Reset connection for SSH switchover
ansible.builtin.meta: reset_connection
- name: Verify SSH connectivity
ansible.builtin.wait_for_connection:
timeout: 30
delay: 2

View File

@@ -0,0 +1,76 @@
---
- name: Wait for connection
ansible.builtin.wait_for_connection:
timeout: "{{ environment_wait_timeout }}"
delay: "{{ environment_wait_delay }}"
- name: Gather facts
ansible.builtin.setup:
- name: Check for live environment markers
ansible.builtin.stat:
path: "{{ item }}"
loop:
- /run/archiso
- /run/live
- /run/initramfs
- /run/initramfs/live
register: environment_live_marker_stat
changed_when: false
- name: Determine root filesystem type
ansible.builtin.set_fact:
environment_root_fstype: >-
{{
ansible_mounts
| selectattr('mount', 'equalto', '/')
| map(attribute='fstype')
| list
| first
| default('')
| lower
}}
environment_archiso_present: >-
{{
(
environment_live_marker_stat.results
| selectattr('item', 'equalto', '/run/archiso')
| selectattr('stat.exists')
| list
| length
) > 0
}}
- name: Identify live environment indicators
ansible.builtin.set_fact:
environment_is_live_environment: >-
{{
(
environment_live_marker_stat.results
| selectattr('stat.exists')
| list
| length
) > 0
or environment_root_fstype in ['overlay', 'overlayfs', 'squashfs', 'aufs']
or (ansible_hostname | default('') | lower is search('live'))
}}
- name: Abort if target is not a live environment
ansible.builtin.assert:
that:
- environment_is_live_environment | bool
fail_msg: |
PRODUCTION SYSTEM DETECTED - ABORTING
The target system does not appear to be a live installer environment.
This playbook must run from a live ISO to avoid wiping production data.
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
quiet: true
- name: Abort if the host is not booted from the Arch install media
when:
- not (custom_iso | bool)
- not environment_archiso_present | bool
ansible.builtin.fail:
msg: This host is not booted from the Arch install media!

View File

@@ -0,0 +1,109 @@
---
- name: Speed-up Bootstrap process
when: not (custom_iso | bool)
ansible.builtin.lineinfile:
path: /etc/pacman.conf
regexp: ^#ParallelDownloads =
line: "ParallelDownloads = {{ environment_parallel_downloads }}"
- name: Wait for pacman lock to be released
when: not (custom_iso | bool)
ansible.builtin.wait_for:
path: /var/lib/pacman/db.lck
state: absent
timeout: "{{ environment_pacman_lock_timeout }}"
changed_when: false
- name: Setup Pacman
when:
- not (custom_iso | bool)
- item.os is not defined or os in item.os
community.general.pacman:
update_cache: true
force: true
name: "{{ item.name }}"
state: latest
loop:
- { name: glibc }
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
- { name: debian-archive-keyring, os: [debian] }
- { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] }
loop_control:
label: "{{ item.name }}"
retries: "{{ environment_pacman_retries }}"
delay: "{{ environment_pacman_retry_delay }}"
- name: Prepare /iso mount and repository for RHEL-based systems
when: os == "rhel"
block:
- name: Create /iso directory
ansible.builtin.file:
path: /usr/local/install/redhat/dvd
state: directory
mode: "0755"
- name: Detect RHEL ISO device
ansible.builtin.command: lsblk -rno NAME,TYPE
register: environment_lsblk_result
changed_when: false
- name: Select RHEL ISO device
vars:
_rom_devices: >-
{{
environment_lsblk_result.stdout_lines
| map('split', ' ')
| selectattr('1', 'equalto', 'rom')
| map('first')
| map('regex_replace', '^', '/dev/')
| list
}}
ansible.builtin.set_fact:
environment_rhel_iso_device: >-
{{
_rom_devices[-1]
if _rom_devices | length > 1
else (_rom_devices[0] | default('/dev/sr1'))
}}
- name: Mount RHEL ISO
ansible.posix.mount:
src: "{{ environment_rhel_iso_device }}"
path: /usr/local/install/redhat/dvd
fstype: iso9660
opts: "ro,loop"
state: mounted
# Security note: RPM Sequoia signature policy is relaxed to allow
# bootstrapping RHEL-family distros from the Arch ISO, where the
# host rpm/dnf does not trust target distro GPG keys. Package
# integrity is verified by the target system's own rpm after reboot.
- name: Create RPM macros directory
when: is_rhel | bool
ansible.builtin.file:
path: /etc/rpm
state: directory
mode: "0755"
- name: Relax RPM Sequoia signature policy for RHEL bootstrap
when: is_rhel | bool
ansible.builtin.copy:
dest: /etc/rpm/macros
content: "%_pkgverify_level none\n"
mode: "0644"
- name: Configure RHEL Repos for installation
when: is_rhel | bool
block:
- name: Create directories for repository files and RPM GPG keys
ansible.builtin.file:
path: /etc/yum.repos.d
state: directory
mode: "0755"
- name: Create RHEL repository file
ansible.builtin.template:
src: "{{ os }}.repo.j2"
dest: /etc/yum.repos.d/{{ os }}.repo
mode: "0644"

View File

@@ -0,0 +1,27 @@
---
- name: Check for third-party preparation tasks
run_once: true
become: false
delegate_to: localhost
vars:
ansible_connection: local
block:
- name: Resolve third-party preparation task path
ansible.builtin.set_fact:
environment_thirdparty_tasks_path: >-
{{
thirdparty_tasks
if thirdparty_tasks | regex_search('^/')
else playbook_dir + '/' + thirdparty_tasks
}}
- name: Stat third-party preparation tasks
ansible.builtin.stat:
path: "{{ environment_thirdparty_tasks_path }}"
register: environment_thirdparty_tasks_stat
- name: Run third-party preparation tasks
when:
- thirdparty_tasks | length > 0
- environment_thirdparty_tasks_stat.stat.exists
ansible.builtin.include_tasks: "{{ environment_thirdparty_tasks_path }}"

View File

@@ -1,250 +1,15 @@
---
- name: Configure work environment
become: "{{ hypervisor_type != 'vmware' }}"
become: "{{ (hypervisor_type | default('none')) != 'vmware' }}"
block:
- name: Wait for connection
ansible.builtin.wait_for_connection:
timeout: 180
delay: 5
- name: Detect and validate live environment
ansible.builtin.include_tasks: _detect_live.yml
- name: Gather facts
ansible.builtin.setup:
- name: Check for live environment markers
ansible.builtin.stat:
path: "{{ item }}"
loop:
- /run/archiso
- /run/live
- /run/initramfs
- /run/initramfs/live
register: environment_live_marker_stat
changed_when: false
- name: Determine root filesystem type
ansible.builtin.set_fact:
environment_root_fstype: >-
{{
ansible_mounts
| selectattr('mount', 'equalto', '/')
| map(attribute='fstype')
| list
| first
| default('')
| lower
}}
environment_archiso_present: >-
{{
(
environment_live_marker_stat.results
| selectattr('item', 'equalto', '/run/archiso')
| selectattr('stat.exists')
| list
| length
) > 0
}}
changed_when: false
- name: Identify live environment indicators
ansible.builtin.set_fact:
environment_is_live_environment: >-
{{
(
environment_live_marker_stat.results
| selectattr('stat.exists')
| list
| length
) > 0
or environment_root_fstype in ['overlay', 'overlayfs', 'squashfs', 'aufs']
or (ansible_hostname | default('') | lower is search('live'))
}}
changed_when: false
- name: Abort if target is not a live environment
ansible.builtin.assert:
that:
- environment_is_live_environment | bool
fail_msg: |
PRODUCTION SYSTEM DETECTED - ABORTING
The target system does not appear to be a live installer environment.
This playbook must run from a live ISO to avoid wiping production data.
Boot from a live installer (Arch, Debian, Ubuntu, etc.) and retry.
quiet: true
- name: Abort if the host is not booted from the Arch install media
when:
- not (custom_iso | bool)
- not environment_archiso_present | bool
ansible.builtin.fail:
msg: This host is not booted from the Arch install media!
- name: Select primary Network Interface
when: hypervisor_type == "vmware"
ansible.builtin.set_fact:
environment_interface_name: >-
{{
(
(ansible_facts.interfaces | default(ansible_facts['ansible_interfaces'] | default([])))
| reject('equalto', 'lo')
| list
| first
)
| default('')
}}
changed_when: false
- name: Set IP-Address
when:
- hypervisor_type == "vmware"
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
ansible.builtin.command: >-
ip addr replace {{ system_cfg.network.ip }}/{{ system_cfg.network.prefix }}
dev {{ environment_interface_name }}
register: environment_ip_result
changed_when: environment_ip_result.rc == 0
- name: Set Default Gateway
when:
- hypervisor_type == "vmware"
- system_cfg.network.gateway is defined and system_cfg.network.gateway | string | length > 0
- system_cfg.network.ip is defined and system_cfg.network.ip | string | length > 0
ansible.builtin.command: "ip route replace default via {{ system_cfg.network.gateway }}"
register: environment_gateway_result
changed_when: environment_gateway_result.rc == 0
- name: Synchronize clock via NTP
ansible.builtin.command: timedatectl set-ntp true
register: environment_ntp_result
changed_when: environment_ntp_result.rc == 0
- name: Configure SSH for root login
when: hypervisor_type == "vmware" and hypervisor_cfg.ssh | bool
block:
- name: Allow login
ansible.builtin.replace:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
replace: "{{ item.replace }}"
loop:
- regexp: "^#?PermitEmptyPasswords.*"
replace: "PermitEmptyPasswords yes"
- regexp: "^#?PermitRootLogin.*"
replace: "PermitRootLogin yes"
- name: Reload SSH service to apply changes
ansible.builtin.service:
name: sshd
state: reloaded
- name: Set SSH connection for VMware
ansible.builtin.set_fact:
ansible_connection: ssh
ansible_user: root
- name: Configure network and connectivity
ansible.builtin.include_tasks: _configure_network.yml
- name: Prepare installer environment
block:
- name: Speed-up Bootstrap process
when: not (custom_iso | bool)
ansible.builtin.lineinfile:
path: /etc/pacman.conf
regexp: ^#ParallelDownloads =
line: ParallelDownloads = 20
- name: Wait for pacman lock to be released
when: not (custom_iso | bool)
ansible.builtin.wait_for:
path: /var/lib/pacman/db.lck
state: absent
timeout: 120
changed_when: false
- name: Setup Pacman
when:
- not (custom_iso | bool)
- item.os is not defined or os in item.os
community.general.pacman:
update_cache: true
force: true
name: "{{ item.name }}"
state: latest
loop:
- { name: glibc }
- { name: dnf, os: [almalinux, fedora, rhel, rocky] }
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
- { name: debian-archive-keyring, os: [debian] }
- { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] }
retries: 4
delay: 15
- name: Prepare /iso mount and repository for RHEL-based systems
when: os == "rhel"
block:
- name: Create /iso directory
ansible.builtin.file:
path: /usr/local/install/redhat/dvd
state: directory
mode: "0755"
- name: Select RHEL ISO device
ansible.builtin.set_fact:
environment_rhel_iso_device: >-
{{
'/dev/sr2'
if hypervisor_type == 'libvirt'
else '/dev/sr1'
}}
changed_when: false
- name: Mount RHEL ISO
ansible.posix.mount:
src: "{{ environment_rhel_iso_device }}"
path: /usr/local/install/redhat/dvd
fstype: iso9660
opts: "ro,loop"
state: mounted
- name: Configure RHEL Repos for installation
when: is_rhel | bool
block:
- name: Create directories for repository files and RPM GPG keys
ansible.builtin.file:
path: /etc/yum.repos.d
state: directory
mode: "0755"
- name: Create RHEL repository file
ansible.builtin.template:
src: "{{ os }}.repo.j2"
dest: /etc/yum.repos.d/{{ os }}.repo
mode: "0644"
- name: Check for third-party preparation tasks
run_once: true
become: false
delegate_to: localhost
vars:
ansible_connection: local
block:
- name: Resolve third-party preparation task path
ansible.builtin.set_fact:
environment_thirdparty_tasks_path: >-
{{
thirdparty_tasks
if thirdparty_tasks | regex_search('^/')
else playbook_dir + '/' + thirdparty_tasks
}}
changed_when: false
- name: Stat third-party preparation tasks
ansible.builtin.stat:
path: "{{ environment_thirdparty_tasks_path }}"
register: environment_thirdparty_tasks_stat
changed_when: false
ansible.builtin.include_tasks: _prepare_installer.yml
- name: Run third-party preparation tasks
when:
- thirdparty_tasks | length > 0
- environment_thirdparty_tasks_stat.stat.exists
ansible.builtin.include_tasks: "{{ environment_thirdparty_tasks_path }}"
ansible.builtin.include_tasks: _thirdparty.yml

View File

@@ -1,4 +1,4 @@
[alma-appstream]
[appstream]
name=AlmaLinux $releasever - AppStream
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/appstream
# baseurl=https://repo.almalinux.org/almalinux/$releasever/AppStream/$basearch/os/
@@ -9,7 +9,7 @@ gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
metadata_expire=86400
enabled_metadata=1
[alma-baseos]
[baseos]
name=AlmaLinux $releasever - BaseOS
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/baseos
# baseurl=https://repo.almalinux.org/almalinux/$releasever/BaseOS/$basearch/os/
@@ -20,7 +20,7 @@ gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
metadata_expire=86400
enabled_metadata=1
[alma-extras]
[extras]
name=AlmaLinux $releasever - Extras
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/extras
# baseurl=https://repo.almalinux.org/almalinux/$releasever/extras/$basearch/os/
@@ -31,7 +31,7 @@ gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-$releasever
metadata_expire=86400
enabled_metadata=0
[alma-highavailability]
[highavailability]
name=AlmaLinux $releasever - HighAvailability
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/highavailability
# baseurl=https://repo.almalinux.org/almalinux/$releasever/HighAvailability/$basearch/os/

View File

@@ -0,0 +1,21 @@
[baseos]
name=Rocky Linux $releasever - BaseOS
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=BaseOS-$releasever
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/BaseOS/$basearch/os/
gpgcheck=1
enabled=1
countme=1
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-$releasever
metadata_expire=86400
enabled_metadata=1
[appstream]
name=Rocky Linux $releasever - AppStream
mirrorlist=https://mirrors.rockylinux.org/mirrorlist?arch=$basearch&repo=AppStream-$releasever
#baseurl=http://dl.rockylinux.org/$contentdir/$releasever/AppStream/$basearch/os/
gpgcheck=1
enabled=1
countme=1
gpgkey=https://dl.rockylinux.org/pub/rocky/RPM-GPG-KEY-Rocky-$releasever
metadata_expire=86400
enabled_metadata=1

View File

@@ -1,4 +1,43 @@
---
# OS family lists — single source of truth for platform detection and validation
os_family_rhel:
- almalinux
- fedora
- rhel
- rocky
os_family_debian:
- debian
- ubuntu
- ubuntu-lts
# OS → family mapping — aligns with the main project's ansible_os_family pattern.
# Enables platform_config dict lookups per role instead of inline when: is_rhel chains.
os_family_map:
almalinux: RedHat
alpine: Alpine
archlinux: Archlinux
debian: Debian
fedora: RedHat
opensuse: Suse
rhel: RedHat
rocky: RedHat
ubuntu: Debian
ubuntu-lts: Debian
void: Void
os_supported:
- almalinux
- alpine
- archlinux
- debian
- fedora
- opensuse
- rhel
- rocky
- ubuntu
- ubuntu-lts
- void
# User input. Normalized into hypervisor_cfg + hypervisor_type.
hypervisor:
type: "none"
@@ -7,13 +46,15 @@ hypervisor_defaults:
url: ""
username: ""
password: ""
host: ""
node: ""
storage: ""
datacenter: ""
cluster: ""
folder: ""
certs: false
ssh: false
physical_default_os: "archlinux"
custom_iso: false
thirdparty_tasks: "dropins/preparation.yml"
@@ -21,7 +62,7 @@ system_defaults:
type: "virtual" # virtual|physical
os: ""
version: ""
filesystem: ""
filesystem: "ext4"
name: ""
id: ""
cpus: 0
@@ -41,11 +82,13 @@ system_defaults:
timezone: "Europe/Vienna"
locale: "en_US.UTF-8"
keymap: "us"
mirror: ""
packages: []
disks: []
users: []
users: {}
root:
password: ""
shell: "/bin/bash"
luks:
enabled: false
passphrase: ""
@@ -83,9 +126,52 @@ system_defaults:
banner:
motd: false
sudo: true
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
# Per-hypervisor required fields — drives data-driven validation.
# All virtual types additionally require network bridge or interfaces.
hypervisor_required_fields:
proxmox:
hypervisor: [url, username, password, node, storage]
system: [id]
vmware:
hypervisor: [url, username, password, datacenter, storage]
system: []
xen:
hypervisor: []
system: []
libvirt:
hypervisor: []
system: []
# Hypervisor-to-disk device prefix mapping for virtual machines.
# Physical installs must set system.disks[].device explicitly.
hypervisor_disk_device_map:
libvirt: "/dev/vd"
xen: "/dev/xvd"
proxmox: "/dev/sd"
vmware: "/dev/sd"
# Mountpoints managed by the partitioning role — forbidden for extra disks.
reserved_mounts:
- /boot
- /boot/efi
- /home
- /var
- /var/log
- /var/log/audit
# Drive letter sequence for disk device naming (max 26 disks).
disk_letter_map: "abcdefghijklmnopqrstuvwxyz"
system_disk_defaults:
size: 0
device: ""

View File

@@ -0,0 +1,100 @@
---
- name: Normalize system disks input
vars:
system_disks: "{{ system_cfg.disks | default([]) }}"
system_disk_letter_map: "{{ disk_letter_map }}"
system_disk_device_prefix: >-
{{
hypervisor_disk_device_map.get(hypervisor_type, '')
if system_cfg.type == 'virtual'
else ''
}}
block:
- name: Validate system disks structure
ansible.builtin.assert:
that:
- system_disks is sequence
- (system_disks | length) <= 26
fail_msg: "system.disks must be a list with at most 26 entries."
quiet: true
- name: Validate system disk entries
ansible.builtin.assert:
that:
- item is mapping
- item.mount is not defined or item.mount is mapping
fail_msg: "Each disk entry must be a dictionary, and disk.mount (if set) must be a dictionary."
quiet: true
loop: "{{ system_disks }}"
loop_control:
label: "{{ item | to_json }}"
- name: Initialize normalized disk list
ansible.builtin.set_fact:
system_disks_cfg: []
- name: Build normalized system disk configuration
vars:
disk_idx: "{{ ansible_loop.index0 }}"
disk_letter: "{{ system_disk_letter_map[disk_idx] }}"
disk_cfg_base: "{{ system_disk_defaults | combine(item, recursive=True) }}"
disk_mount: "{{ system_disk_defaults.mount | combine((disk_cfg_base.mount | default({})), recursive=True) }}"
disk_mount_path: "{{ (disk_mount.path | default('') | string) | trim }}"
disk_mount_fstype: >-
{{
disk_mount.fstype
if (disk_mount.fstype | default('') | string | length) > 0
else ('ext4' if disk_mount_path | length > 0 else '')
}}
disk_device: >-
{{
disk_cfg_base.device
if (disk_cfg_base.device | string | length) > 0
else (
(system_disk_device_prefix ~ disk_letter)
if system_cfg.type == 'virtual'
else ''
)
}}
disk_partition: >-
{{
disk_device ~ ('p1' if (disk_device | regex_search('\\d$')) else '1')
if disk_device | length > 0
else ''
}}
ansible.builtin.set_fact:
system_disks_cfg: >-
{{
system_disks_cfg + [
disk_cfg_base
| combine(
{
'device': disk_device,
'mount': {
'path': disk_mount_path,
'fstype': disk_mount_fstype,
'label': disk_mount.label | default('') | string,
'opts': disk_mount.opts | default('defaults') | string
},
'partition': disk_partition
},
recursive=True
)
]
}}
loop: "{{ system_disks }}"
loop_control:
loop_var: item
extended: true
label: "{{ item | to_json }}"
- name: Update system configuration with normalized disks
ansible.builtin.set_fact:
system_cfg: "{{ system_cfg | combine({'disks': system_disks_cfg}, recursive=True) }}"
- name: Set install_drive from primary disk
when:
- system_disks_cfg | length > 0
- system_disks_cfg[0].device | string | length > 0
ansible.builtin.set_fact:
install_drive: "{{ system_disks_cfg[0].device }}"

View File

@@ -0,0 +1,170 @@
---
- name: Build normalized system configuration
vars:
system_raw: "{{ system_defaults | combine(system, recursive=True) }}"
system_type: "{{ 'virtual' if (system_raw.type | string | lower) in ['vm', 'virtual'] else (system_raw.type | string | lower) }}"
system_os_input: "{{ system_raw.os | default('') | string | lower }}"
system_name: >-
{{
system_raw.name | string | trim
if (system_raw.name | default('') | string | trim | length) > 0
else inventory_hostname
}}
_mirror_defaults:
debian: "https://deb.debian.org/debian/"
ubuntu: "http://archive.ubuntu.com/ubuntu/"
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
ansible.builtin.set_fact:
system_cfg:
# --- Identity & platform ---
type: "{{ system_type }}"
os: "{{ system_os_input if system_os_input | length > 0 else (physical_default_os if system_type == 'physical' else '') }}"
version: "{{ system_raw.version | default('') | string }}"
filesystem: "{{ system_raw.filesystem | default('') | string | lower }}"
name: "{{ system_name }}"
id: "{{ system_raw.id | default('') | string }}"
# --- VM sizing (ignored for physical) ---
cpus: "{{ [system_raw.cpus | default(0) | int, 0] | max }}"
memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}"
balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}"
# --- Network ---
# Flat fields (bridge, ip, etc.) and interfaces[] are mutually exclusive.
# When interfaces[] is set, flat fields are populated from the first
# interface in the "Populate primary network fields" task below.
# When only flat fields are set, a synthetic interfaces[] entry is built.
network:
bridge: "{{ system_raw.network.bridge | default('') | string }}"
vlan: "{{ system_raw.network.vlan | default('') | string }}"
ip: "{{ system_raw.network.ip | default('') | string }}"
prefix: >-
{{
(system_raw.network.prefix | int | string)
if (system_raw.network.prefix | default('') | string | length) > 0
else ''
}}
gateway: "{{ system_raw.network.gateway | default('') | string }}"
dns:
servers: "{{ system_raw.network.dns.servers | default([]) }}"
search: "{{ system_raw.network.dns.search | default([]) }}"
interfaces: >-
{{
system_raw.network.interfaces
if (system_raw.network.interfaces | default([]) | length > 0)
else (
[{
'name': '',
'bridge': system_raw.network.bridge | default('') | string,
'vlan': system_raw.network.vlan | default('') | string,
'ip': system_raw.network.ip | default('') | string,
'prefix': (
(system_raw.network.prefix | int | string)
if (system_raw.network.prefix | default('') | string | length) > 0
else ''
),
'gateway': system_raw.network.gateway | default('') | string
}]
if (system_raw.network.bridge | default('') | string | length > 0)
else []
)
}}
# --- Locale & environment ---
timezone: "{{ system_raw.timezone | string }}"
locale: "{{ system_raw.locale | string }}"
keymap: "{{ system_raw.keymap | string }}"
mirror: >-
{{
system_raw.mirror | string | trim
if (system_raw.mirror | default('') | string | trim | length) > 0
else _mirror_defaults[system_raw.os | default('') | string | lower] | default('')
}}
path: >-
{{
(system_raw.path | default('') | string)
if (system_raw.path | default('') | string | length > 0)
else (hypervisor_cfg.folder | default('') | string)
}}
packages: >-
{{
(
system_raw.packages
if system_raw.packages is iterable and system_raw.packages is not string
else (system_raw.packages | string).split(',')
)
| map('trim')
| reject('equalto', '')
| list
}}
# --- Storage & accounts ---
disks: "{{ system_raw.disks | default([]) }}"
users: "{{ system_raw.users | default({}) }}"
root:
password: "{{ system_raw.root.password | string }}"
shell: "{{ system_raw.root.shell | default('/bin/bash') | string }}"
# --- LUKS disk encryption ---
luks:
enabled: "{{ system_raw.luks.enabled | bool }}"
passphrase: "{{ system_raw.luks.passphrase | string }}"
mapper: "{{ system_raw.luks.mapper | string }}"
auto: "{{ system_raw.luks.auto | bool }}"
method: "{{ system_raw.luks.method | string | lower }}"
tpm2:
device: "{{ system_raw.luks.tpm2.device | string }}"
pcrs: "{{ system_raw.luks.tpm2.pcrs | string }}"
keysize: "{{ system_raw.luks.keysize | int }}"
options: "{{ system_raw.luks.options | string }}"
type: "{{ system_raw.luks.type | string }}"
cipher: "{{ system_raw.luks.cipher | string }}"
hash: "{{ system_raw.luks.hash | string }}"
iter: "{{ system_raw.luks.iter | int }}"
bits: "{{ system_raw.luks.bits | int }}"
pbkdf: "{{ system_raw.luks.pbkdf | string }}"
urandom: "{{ system_raw.luks.urandom | bool }}"
verify: "{{ system_raw.luks.verify | bool }}"
# --- Feature flags ---
features:
cis:
enabled: "{{ system_raw.features.cis.enabled | bool }}"
selinux:
enabled: "{{ system_raw.features.selinux.enabled | bool }}"
firewall:
enabled: "{{ system_raw.features.firewall.enabled | bool }}"
backend: "{{ system_raw.features.firewall.backend | string | lower }}"
toolkit: "{{ system_raw.features.firewall.toolkit | string | lower }}"
ssh:
enabled: "{{ system_raw.features.ssh.enabled | bool }}"
zstd:
enabled: "{{ system_raw.features.zstd.enabled | bool }}"
swap:
enabled: "{{ system_raw.features.swap.enabled | bool }}"
banner:
motd: "{{ system_raw.features.banner.motd | bool }}"
sudo: "{{ system_raw.features.banner.sudo | bool }}"
rhel_repo:
source: "{{ system_raw.features.rhel_repo.source | default('iso') | string | lower }}"
url: "{{ system_raw.features.rhel_repo.url | default('') | string }}"
chroot:
tool: "{{ system_raw.features.chroot.tool | string }}"
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

@@ -0,0 +1,57 @@
---
- name: Ensure system input is a dictionary
ansible.builtin.set_fact:
system: "{{ system | default({}) }}"
- name: Validate system input types
ansible.builtin.assert:
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.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."
quiet: true
- name: Validate DNS lists (not strings)
when: system.network is defined and system.network.dns is defined
ansible.builtin.assert:
that:
- system.network.dns.servers is not defined or (system.network.dns.servers is iterable and system.network.dns.servers is not string)
- system.network.dns.search is not defined or (system.network.dns.search is iterable and system.network.dns.search is not string)
fail_msg: "system.network.dns.servers and system.network.dns.search must be lists, not strings."
quiet: true
- name: Validate system.users entries
when: system.users is defined and system.users is mapping 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."
quiet: true
loop: "{{ system.users | dict2items }}"
loop_control:
label: "{{ item.key }}"
- name: Validate system features input types
when: system.features is defined
loop: "{{ system_defaults.features | dict2items | map(attribute='key') | list }}"
loop_control:
label: "system.features.{{ item }}"
ansible.builtin.assert:
that:
- (system.features[item] | default({})) is mapping
fail_msg: "system.features.{{ item }} must be a dictionary."
quiet: true
- name: Validate system LUKS TPM2 input type
when: system.luks is defined and system.luks is mapping
ansible.builtin.assert:
that:
- system.luks.tpm2 is not defined or system.luks.tpm2 is mapping
fail_msg: "system.luks.tpm2 must be a dictionary."
quiet: true

View File

@@ -1,20 +1,24 @@
---
- name: Ensure hypervisor input is a dictionary
ansible.builtin.set_fact:
hypervisor: "{{ hypervisor | default({}) }}"
- name: Validate hypervisor input
ansible.builtin.assert:
that:
- hypervisor is mapping
- hypervisor.type is defined
- hypervisor.type | string | length > 0
fail_msg: "hypervisor must be a dictionary and hypervisor.type must be set (e.g. libvirt|proxmox|vmware|xen|none)."
quiet: true
- name: Normalize hypervisor configuration
vars:
merged: "{{ hypervisor_defaults | combine(hypervisor, recursive=True) }}"
ansible.builtin.set_fact:
hypervisor_cfg: "{{ merged }}"
hypervisor_type: "{{ merged.type | string | lower }}"
when: hypervisor_cfg is not defined
block:
- name: Ensure hypervisor input is a dictionary
ansible.builtin.set_fact:
hypervisor: "{{ hypervisor | default({}) }}"
- name: Validate hypervisor input
ansible.builtin.assert:
that:
- hypervisor is mapping
- hypervisor.type is defined
- hypervisor.type | string | length > 0
fail_msg: "hypervisor must be a dictionary and hypervisor.type must be set (e.g. libvirt|proxmox|vmware|xen|none)."
quiet: true
- name: Merge hypervisor defaults with input
vars:
merged: "{{ hypervisor_defaults | combine(hypervisor, recursive=True) }}"
ansible.builtin.set_fact:
hypervisor_cfg: "{{ merged }}"
hypervisor_type: "{{ merged.type | string | lower }}"
no_log: true

View File

@@ -1,4 +1,8 @@
---
# Centralized normalization — all input dicts (system, hypervisor, disks)
# are normalized here into system_cfg, hypervisor_cfg, etc.
# Downstream roles consume these computed facts directly and do NOT need
# per-role _normalize.yml (except CIS, which has its own input dict).
- name: Global defaults loaded
ansible.builtin.debug:
msg: Global defaults loaded.
@@ -6,16 +10,49 @@
- name: Normalize hypervisor inputs
ansible.builtin.include_tasks: hypervisor.yml
- name: Set VMware module auth defaults
when: hypervisor_type == 'vmware'
ansible.builtin.set_fact:
_vmware_auth:
hostname: "{{ hypervisor_cfg.url }}"
username: "{{ hypervisor_cfg.username }}"
password: "{{ hypervisor_cfg.password }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
no_log: true
- name: Set Proxmox module auth defaults
when: hypervisor_type == 'proxmox'
ansible.builtin.set_fact:
_proxmox_auth:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
_proxmox_auth_node:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.node }}"
no_log: true
- name: Normalize system inputs
ansible.builtin.include_tasks: system.yml
- name: Inherit folder from hypervisor when system path is empty
when:
- system_cfg.path | default('') | string | length == 0
- hypervisor_cfg.folder | default('') | string | length > 0
ansible.builtin.set_fact:
system_cfg: "{{ system_cfg | combine({'path': hypervisor_cfg.folder | string}, recursive=True) }}"
- name: Validate variables
ansible.builtin.include_tasks: validation.yml
- name: Set OS family flags
ansible.builtin.set_fact:
is_rhel: "{{ os in ['almalinux', 'fedora', 'rhel', 'rocky'] }}"
is_debian: "{{ os in ['debian', 'ubuntu', 'ubuntu-lts'] }}"
is_rhel: "{{ os in os_family_rhel }}"
is_debian: "{{ os in os_family_debian }}"
os_family: "{{ os_family_map[os] | default('Unknown') }}"
- name: Normalize OS version for keying
when:
@@ -44,13 +81,27 @@
when:
- system_cfg.type == "virtual"
- hypervisor_type != "vmware"
vars:
_primary: "{{ (system_cfg.users | dict2items | selectattr('value.password', 'defined') | first) }}"
ansible.builtin.set_fact:
ansible_user: "{{ system_cfg.users[0].name }}"
ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_host: "{{ system_cfg.network.ip }}"
ansible_port: 22
ansible_user: "{{ _primary.key }}"
ansible_password: "{{ _primary.value.password }}"
ansible_become_password: "{{ _primary.value.password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
no_log: true
- name: Set connection for VMware
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_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 }}"
no_log: true

View File

@@ -1,183 +1,58 @@
---
- name: Ensure system input is a dictionary
# Two code paths:
# 1. Fresh run (system_cfg undefined): normalize from raw `system` input.
# 2. Pre-computed (system_cfg already set, e.g. from main project's deploy_iac):
# merge with bootstrap system_defaults to fill missing fields (luks, features,
# etc.) that bootstrap expects but the main project doesn't set, then derive
# convenience facts (hostname, os, os_version).
- name: Normalize system and disk configuration
when: system_cfg is not defined
block:
- name: Validate raw system input types
ansible.builtin.include_tasks: _validate_input.yml
- name: Normalize system configuration
ansible.builtin.include_tasks: _normalize_system.yml
- name: Normalize disk configuration
ansible.builtin.include_tasks: _normalize_disks.yml
- name: Check if pre-computed system_cfg needs enrichment
when: system_cfg is defined
ansible.builtin.set_fact:
system: "{{ system | default({}) }}"
_bootstrap_needs_enrichment: "{{ hostname is not defined }}"
- name: Validate system input types
ansible.builtin.assert:
that:
- system is mapping
- system.network is not defined or system.network is mapping
- system.users is not defined or (system.users is iterable and system.users is not string and system.users is not mapping)
- system.root is not defined or system.root is mapping
- system.luks is not defined or system.luks is mapping
- system.features is not defined or system.features is mapping
fail_msg: "system and its nested keys (network, root, luks, features) must be dictionaries; system.users must be a list."
quiet: true
- name: Validate DNS lists (not strings)
when: system.network is defined and system.network.dns is defined
ansible.builtin.assert:
that:
- system.network.dns.servers is not defined or (system.network.dns.servers is iterable and system.network.dns.servers is not string)
- system.network.dns.search is not defined or (system.network.dns.search is iterable and system.network.dns.search is not string)
fail_msg: "system.network.dns.servers and system.network.dns.search must be lists, not strings."
quiet: true
- name: Validate system.users entries
when: system.users is defined and system.users | length > 0
ansible.builtin.assert:
that:
- item is mapping
- item.name is defined and (item.name | string | length) > 0
- item.keys is not defined or (item.keys is iterable and item.keys is not string)
fail_msg: "Each system.users[] entry must be a dict with 'name'; 'keys' must be a list."
quiet: true
loop: "{{ system.users }}"
loop_control:
label: "{{ item.name | default('(unnamed)') }}"
- name: Validate system features input types
when: system.features is defined
loop: "{{ system_defaults.features | dict2items | map(attribute='key') | list }}"
loop_control:
label: "system.features.{{ item }}"
ansible.builtin.assert:
that:
- (system.features[item] | default({})) is mapping
fail_msg: "system.features.{{ item }} must be a dictionary."
quiet: true
- name: Validate system LUKS TPM2 input type
when: system.luks is defined and system.luks is mapping
ansible.builtin.assert:
that:
- system.luks.tpm2 is not defined or system.luks.tpm2 is mapping
fail_msg: "system.luks.tpm2 must be a dictionary."
quiet: true
- name: Build normalized system configuration
vars:
system_raw: "{{ system_defaults | combine(system, recursive=True) }}"
system_type: "{{ system_raw.type | string | lower }}"
system_os_input: "{{ system_raw.os | default('') | string | lower }}"
system_name: >-
{{
system_raw.name | string | trim
if (system_raw.name | default('') | string | trim | length) > 0
else inventory_hostname
}}
ansible.builtin.set_fact:
system_cfg:
type: "{{ system_type }}"
os: "{{ system_os_input if system_os_input | length > 0 else ('archlinux' if system_type == 'physical' else '') }}"
version: "{{ system_raw.version | default('') | string }}"
filesystem: "{{ system_raw.filesystem | default('') | string | lower }}"
name: "{{ system_name }}"
id: "{{ system_raw.id | default('') | string }}"
cpus: "{{ [system_raw.cpus | default(0) | int, 0] | max }}"
memory: "{{ [system_raw.memory | default(0) | int, 0] | max }}"
balloon: "{{ [system_raw.balloon | default(0) | int, 0] | max }}"
network:
bridge: "{{ system_raw.network.bridge | default('') | string }}"
vlan: "{{ system_raw.network.vlan | default('') | string }}"
ip: "{{ system_raw.network.ip | default('') | string }}"
prefix: >-
{{
(system_raw.network.prefix | int)
if (system_raw.network.prefix | default('') | string | length) > 0
else ''
}}
gateway: "{{ system_raw.network.gateway | default('') | string }}"
dns:
servers: "{{ system_raw.network.dns.servers | default([]) }}"
search: "{{ system_raw.network.dns.search | default([]) }}"
interfaces: >-
{{
system_raw.network.interfaces
if (system_raw.network.interfaces | default([]) | length > 0)
else (
[{
'name': 'eth0',
'bridge': system_raw.network.bridge | default('') | string,
'vlan': system_raw.network.vlan | default('') | string,
'ip': system_raw.network.ip | default('') | string,
'prefix': (
(system_raw.network.prefix | int | string)
if (system_raw.network.prefix | default('') | string | length) > 0
else ''
),
'gateway': system_raw.network.gateway | default('') | string
}]
if (system_raw.network.bridge | default('') | string | length > 0)
else []
)
}}
timezone: "{{ system_raw.timezone | default('Europe/Vienna') | string }}"
locale: "{{ system_raw.locale | default('en_US.UTF-8') | string }}"
keymap: "{{ system_raw.keymap | default('us') | string }}"
path: "{{ system_raw.path | default('') | string }}"
packages: >-
{{
(
system_raw.packages
if system_raw.packages is iterable and system_raw.packages is not string
else (system_raw.packages | string).split(',')
)
| map('trim')
| reject('equalto', '')
| list
}}
disks: "{{ system_raw.disks | default([]) }}"
users: "{{ system_raw.users | default([]) }}"
root:
password: "{{ system_raw.root.password | string }}"
luks:
enabled: "{{ system_raw.luks.enabled | bool }}"
passphrase: "{{ system_raw.luks.passphrase | string }}"
mapper: "{{ system_raw.luks.mapper | string }}"
auto: "{{ system_raw.luks.auto | bool }}"
method: "{{ system_raw.luks.method | string | lower }}"
tpm2:
device: "{{ system_raw.luks.tpm2.device | string }}"
pcrs: "{{ system_raw.luks.tpm2.pcrs | string }}"
keysize: "{{ system_raw.luks.keysize | int }}"
options: "{{ system_raw.luks.options | string }}"
type: "{{ system_raw.luks.type | string }}"
cipher: "{{ system_raw.luks.cipher | string }}"
hash: "{{ system_raw.luks.hash | string }}"
iter: "{{ system_raw.luks.iter | int }}"
bits: "{{ system_raw.luks.bits | int }}"
pbkdf: "{{ system_raw.luks.pbkdf | string }}"
urandom: "{{ system_raw.luks.urandom | bool }}"
verify: "{{ system_raw.luks.verify | bool }}"
features:
cis:
enabled: "{{ system_raw.features.cis.enabled | bool }}"
selinux:
enabled: "{{ system_raw.features.selinux.enabled | bool }}"
firewall:
enabled: "{{ system_raw.features.firewall.enabled | bool }}"
backend: "{{ system_raw.features.firewall.backend | string | lower }}"
toolkit: "{{ system_raw.features.firewall.toolkit | string | lower }}"
ssh:
enabled: "{{ system_raw.features.ssh.enabled | bool }}"
zstd:
enabled: "{{ system_raw.features.zstd.enabled | bool }}"
swap:
enabled: "{{ system_raw.features.swap.enabled | bool }}"
banner:
motd: "{{ system_raw.features.banner.motd | bool }}"
sudo: "{{ system_raw.features.banner.sudo | bool }}"
chroot:
tool: "{{ system_raw.features.chroot.tool | string }}"
hostname: "{{ system_name }}"
os: "{{ system_os_input if system_os_input | length > 0 else ('archlinux' if system_type == 'physical' else '') }}"
os_version: "{{ system_raw.version | default('') | string }}"
- name: Populate primary network fields from first interface
- name: Merge pre-computed system_cfg with bootstrap system_defaults
when:
- system_cfg.network.interfaces | length > 0
- system_cfg is defined
- _bootstrap_needs_enrichment | default(false) | bool
ansible.builtin.set_fact:
system_cfg: "{{ system_defaults | combine(system | default({}), recursive=True) | combine(system_cfg, recursive=True) }}"
- name: Apply mirror default for pre-computed system_cfg
when:
- system_cfg is defined
- _bootstrap_needs_enrichment | default(false) | bool
- system_cfg.mirror | default('') | string | trim | length == 0
vars:
# Same as _normalize_system.yml — kept in sync manually.
_mirror_defaults:
debian: "https://deb.debian.org/debian/"
ubuntu: "http://archive.ubuntu.com/ubuntu/"
ubuntu-lts: "http://archive.ubuntu.com/ubuntu/"
ansible.builtin.set_fact:
system_cfg: >-
{{
system_cfg | combine({
'mirror': _mirror_defaults[system_cfg.os | default('') | string | lower] | default('')
}, recursive=True)
}}
- name: Populate primary network fields from first interface (pre-computed)
when:
- system_cfg is defined
- _bootstrap_needs_enrichment | default(false) | bool
- system_cfg.network.interfaces | default([]) | length > 0
- system_cfg.network.bridge | default('') | string | length == 0
vars:
_primary: "{{ system_cfg.network.interfaces[0] }}"
@@ -195,103 +70,17 @@
}, recursive=True)
}}
- name: Normalize system disks input
vars:
system_disks: "{{ system_cfg.disks | default([]) }}"
system_disk_letter_map: "abcdefghijklmnopqrstuvwxyz"
system_disk_device_prefix: >-
{{
{'libvirt': '/dev/vd', 'xen': '/dev/xvd', 'proxmox': '/dev/sd', 'vmware': '/dev/sd'}.get(hypervisor_type, '')
if system_cfg.type == 'virtual'
else ''
}}
block:
- name: Validate system disks structure
ansible.builtin.assert:
that:
- system_disks is sequence
- (system_disks | length) <= 26
fail_msg: "system.disks must be a list with at most 26 entries."
quiet: true
- name: Validate system disk entries
ansible.builtin.assert:
that:
- item is mapping
- item.mount is not defined or item.mount is mapping
fail_msg: "Each disk entry must be a dictionary, and disk.mount (if set) must be a dictionary."
quiet: true
loop: "{{ system_disks }}"
loop_control:
label: "{{ item | to_json }}"
- name: Initialize normalized disk list
ansible.builtin.set_fact:
system_disks_cfg: []
- name: Build normalized system disk configuration
vars:
disk_idx: "{{ ansible_loop.index0 }}"
disk_letter: "{{ system_disk_letter_map[disk_idx] }}"
disk_cfg_base: "{{ system_disk_defaults | combine(item, recursive=True) }}"
disk_mount: "{{ system_disk_defaults.mount | combine((disk_cfg_base.mount | default({})), recursive=True) }}"
disk_mount_path: "{{ (disk_mount.path | default('') | string) | trim }}"
disk_mount_fstype: >-
{{
disk_mount.fstype
if (disk_mount.fstype | default('') | string | length) > 0
else ('ext4' if disk_mount_path | length > 0 else '')
}}
disk_device: >-
{{
disk_cfg_base.device
if (disk_cfg_base.device | string | length) > 0
else (
(system_disk_device_prefix ~ disk_letter)
if system_cfg.type == 'virtual'
else ''
)
}}
disk_partition: >-
{{
disk_device ~ ('p1' if (disk_device | regex_search('\\d$')) else '1')
if disk_device | length > 0
else ''
}}
ansible.builtin.set_fact:
system_disks_cfg: >-
{{
system_disks_cfg + [
disk_cfg_base
| combine(
{
'device': disk_device,
'mount': {
'path': disk_mount_path,
'fstype': disk_mount_fstype,
'label': disk_mount.label | default('') | string,
'opts': disk_mount.opts | default('defaults') | string
},
'partition': disk_partition
},
recursive=True
)
]
}}
loop: "{{ system_disks }}"
loop_control:
loop_var: item
extended: true
label: "{{ item | to_json }}"
- name: Update system configuration with normalized disks
ansible.builtin.set_fact:
system_cfg: "{{ system_cfg | combine({'disks': system_disks_cfg}, recursive=True) }}"
- name: Set install_drive from primary disk
when:
- system_disks_cfg | length > 0
- system_disks_cfg[0].device | string | length > 0
ansible.builtin.set_fact:
install_drive: "{{ system_disks_cfg[0].device }}"
- name: Derive convenience facts from pre-computed system_cfg
when:
- system_cfg is defined
- _bootstrap_needs_enrichment | default(false) | bool
ansible.builtin.set_fact:
hostname: "{{ system_cfg.name | default(inventory_hostname) }}"
os: "{{ system_cfg.os | default('') }}"
os_version: "{{ system_cfg.version | default('') | string }}"
- name: Normalize disk configuration (pre-computed system_cfg)
when:
- system_cfg is defined
- install_drive is not defined
ansible.builtin.include_tasks: _normalize_disks.yml

View File

@@ -114,7 +114,7 @@
ansible.builtin.assert:
that:
- os is defined
- os in ["almalinux", "alpine", "archlinux", "debian", "fedora", "opensuse", "rhel", "rocky", "ubuntu", "ubuntu-lts", "void"]
- os in os_supported
- >-
os not in ["debian", "fedora", "rocky", "almalinux", "rhel"]
or (os_version is defined and (os_version | string | length) > 0)
@@ -123,7 +123,7 @@
or (
os == "debian" and (os_version | string) in ["10", "11", "12", "13", "unstable"]
) or (
os == "fedora" and (os_version | string) in ["40", "41", "42", "43"]
os == "fedora" and (os_version | int) >= 38 and (os_version | int) <= 43
) or (
os in ["rocky", "almalinux"]
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
@@ -131,7 +131,16 @@
os == "rhel"
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
) or (
os in ["alpine", "archlinux", "opensuse", "ubuntu", "ubuntu-lts", "void"]
os == "ubuntu"
and (os_version | string) is match("^(2[0-9])\\.04$")
) or (
os == "ubuntu-lts"
and (os_version | string) is match("^(2[0-9])\\.04$")
) or (
os in ["ubuntu", "ubuntu-lts"]
and (os_version | default('') | string | length) == 0
) or (
os in ["alpine", "archlinux", "opensuse", "void"]
)
fail_msg: "Invalid os/version specified. Please check README.md for supported values."
quiet: true
@@ -143,56 +152,60 @@
fail_msg: "rhel_iso is required when os=rhel."
quiet: true
- name: Validate Proxmox hypervisor inputs
- name: Validate hypervisor-specific required fields
when:
- system_cfg.type == "virtual"
- hypervisor_type == "proxmox"
- hypervisor_type in hypervisor_required_fields
ansible.builtin.assert:
that:
- hypervisor_cfg.url | string | length > 0
- hypervisor_cfg.username | string | length > 0
- hypervisor_cfg.password | string | length > 0
- hypervisor_cfg.host | string | length > 0
- hypervisor_cfg.storage | string | length > 0
- system_cfg.id | string | length > 0
- >-
(system_cfg.network.bridge | default('') | string | length > 0)
or (system_cfg.network.interfaces | default([]) | length > 0)
fail_msg: >-
Missing required Proxmox inputs. Define hypervisor.(url,username,password,host,storage),
system.id, and system.network.bridge (or system.network.interfaces[]).
- (hypervisor_cfg[item] | default('') | string | length) > 0
fail_msg: "Missing required {{ hypervisor_type }} field: hypervisor.{{ item }}"
quiet: true
loop: "{{ hypervisor_required_fields[hypervisor_type].hypervisor | default([]) }}"
loop_control:
label: "hypervisor.{{ item }}"
no_log: true
- name: Validate VMware hypervisor inputs
- name: Validate VMware placement (cluster or node required, mutually exclusive)
when:
- system_cfg.type == "virtual"
- hypervisor_type == "vmware"
ansible.builtin.assert:
that:
- hypervisor_cfg.url | string | length > 0
- hypervisor_cfg.username | string | length > 0
- hypervisor_cfg.password | string | length > 0
- hypervisor_cfg.datacenter | string | length > 0
- hypervisor_cfg.cluster | string | length > 0
- hypervisor_cfg.storage | string | length > 0
- >-
(system_cfg.network.bridge | default('') | string | length > 0)
or (system_cfg.network.interfaces | default([]) | length > 0)
(hypervisor_cfg.cluster | default('') | string | length > 0)
or (hypervisor_cfg.node | default('') | string | length > 0)
- >-
(hypervisor_cfg.cluster | default('') | string | length == 0)
or (hypervisor_cfg.node | default('') | string | length == 0)
fail_msg: >-
Missing required VMware inputs. Define hypervisor.(url,username,password,datacenter,cluster,storage)
and system.network.bridge (or system.network.interfaces[]).
VMware requires either hypervisor.cluster or hypervisor.node (mutually exclusive).
cluster targets a vSphere cluster; node targets a specific ESXi host.
quiet: true
- name: Validate Xen hypervisor inputs
- name: Validate hypervisor-specific required system fields
when:
- system_cfg.type == "virtual"
- hypervisor_type == "xen"
- hypervisor_type in hypervisor_required_fields
ansible.builtin.assert:
that:
- (system_cfg[item] | default('') | string | length) > 0
fail_msg: "Missing required {{ hypervisor_type }} field: system.{{ item }}"
quiet: true
loop: "{{ hypervisor_required_fields[hypervisor_type].system | default([]) }}"
loop_control:
label: "system.{{ item }}"
- name: Validate virtual machine network requirement
when: system_cfg.type == "virtual"
ansible.builtin.assert:
that:
- >-
(system_cfg.network.bridge | default('') | string | length > 0)
or (system_cfg.network.interfaces | default([]) | length > 0)
fail_msg: "Missing required Xen inputs. Define system.network.bridge (or system.network.interfaces[])."
fail_msg: >-
Missing required {{ hypervisor_type }} network configuration.
Define system.network.bridge (or system.network.interfaces[]).
quiet: true
- name: Validate virtual installer ISO requirement
@@ -210,6 +223,7 @@
- system_cfg.features.firewall.toolkit is defined
- system_cfg.features.firewall.toolkit in ["iptables", "nftables"]
- system_cfg.features.firewall.enabled is defined
- system_cfg.features.ssh.enabled is defined
- system_cfg.features.banner.motd is defined
- system_cfg.features.banner.sudo is defined
- system_cfg.luks.enabled is defined
@@ -227,6 +241,8 @@
- system_cfg.disks is defined and (system_cfg.disks | length) > 0
- (system_cfg.disks[0].size | float) > 0
- (system_cfg.disks[0].size | float) >= 20
# Btrfs minimum disk: swap_size + 5.5 GiB overhead (subvolumes + metadata).
# Swap sizing: memory < 16 GiB → max(memory_GiB, 2); memory >= 16 GiB → memory/2.
- >-
system_cfg.filesystem != "btrfs"
or (
@@ -236,7 +252,7 @@
(system_cfg.memory | float / 1024 >= 16.0)
| ternary(
(system_cfg.memory | float / 2048),
[system_cfg.memory | float / 1024, 4.0] | max
[system_cfg.memory | float / 1024, 2.0] | max
)
)
+ 5.5
@@ -245,6 +261,31 @@
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 }}"
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."
quiet: true
no_log: true
- name: Validate DNS servers is a list
when:
- system_cfg.network.dns.servers is defined
- system_cfg.network.dns.servers | length > 0
ansible.builtin.assert:
that:
- system_cfg.network.dns.servers is iterable
- system_cfg.network.dns.servers is not string
- system_cfg.network.dns.servers is not mapping
fail_msg: "system.network.dns.servers must be a list."
quiet: true
- name: Validate all virtual disks have a positive size
when: system_cfg.type == "virtual"
ansible.builtin.assert:
@@ -272,8 +313,8 @@
system_disk_mounts: >-
{{
(system_cfg.disks | default([]))
| map(attribute='mount')
| map(attribute='path')
| map(attribute='mount', default={})
| map(attribute='path', default='')
| map('string')
| map('trim')
| reject('equalto', '')
@@ -288,15 +329,11 @@
- name: Validate disk mount definitions
when: system_cfg.disks is defined
vars:
reserved_mounts:
- /boot
- /boot/efi
- /home
- /swap
- /var
- /var/cache/pacman/pkg
- /var/log
- /var/log/audit
all_reserved_mounts: >-
{{
reserved_mounts
+ (['/var/cache/pacman/pkg'] if os == 'archlinux' else [])
}}
disk_mount: "{{ (item.mount.path | default('') | string) | trim }}"
disk_fstype: "{{ (item.mount.fstype | default('') | string) | trim }}"
disk_device: "{{ (item.device | default('') | string) | trim }}"
@@ -305,7 +342,7 @@
that:
- disk_mount == "" or disk_mount.startswith("/")
- disk_mount == "" or disk_mount != "/"
- disk_mount == "" or disk_mount not in reserved_mounts
- disk_mount == "" or disk_mount not in all_reserved_mounts
- disk_mount == "" or disk_fstype in ["btrfs", "ext4", "xfs"]
- disk_mount == "" or system_cfg.type == "virtual" or (disk_device | length) > 0
- disk_mount == "" or system_cfg.type != "virtual" or (disk_size | float) > 0
@@ -321,7 +358,8 @@
that:
- system_cfg.network.prefix is defined
- (system_cfg.network.prefix | int) > 0
fail_msg: "system.network.prefix is required when system.network.ip is set."
- (system_cfg.network.prefix | int) <= 32
fail_msg: "system.network.prefix must be between 1 and 32 when system.network.ip is set."
quiet: true
- name: Validate network interfaces entries
@@ -330,8 +368,48 @@
that:
- item is mapping
- item.bridge is defined and (item.bridge | string | length) > 0
fail_msg: "Each system.network.interfaces[] entry must be a dict with at least a 'bridge' key."
- >-
(item.ip | default('') | string | length) == 0
or (item.prefix | default('') | string | length) > 0
fail_msg: "Each system.network.interfaces[] entry must have a 'bridge' key and 'prefix' when 'ip' is set."
quiet: true
loop: "{{ system_cfg.network.interfaces }}"
loop_control:
label: "{{ item | to_json }}"
- name: Validate hostname format
ansible.builtin.assert:
that:
- hostname is regex("^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$")
fail_msg: "hostname '{{ hostname }}' contains invalid characters. Use only alphanumeric, hyphens, dots, and underscores."
quiet: true
- name: Validate IP address format
when: system_cfg.network.ip is defined and (system_cfg.network.ip | string | length) > 0
ansible.builtin.assert:
that:
- system_cfg.network.ip is regex("^([0-9]{1,3}\\.){3}[0-9]{1,3}$")
fail_msg: "system.network.ip '{{ system_cfg.network.ip }}' is not a valid IPv4 address."
quiet: true
- name: Validate DNS server format
when:
- system_cfg.network.dns.servers is defined
- system_cfg.network.dns.servers | length > 0
ansible.builtin.assert:
that:
- item is regex("^([0-9]{1,3}\\.){3}[0-9]{1,3}$")
fail_msg: "DNS server '{{ item }}' is not a valid IPv4 address."
quiet: true
loop: "{{ system_cfg.network.dns.servers }}"
- name: Validate LUKS method
when: system_cfg.luks.enabled | bool
ansible.builtin.assert:
that:
- system_cfg.luks.method in ["tpm2", "keyfile"]
- >-
(system_cfg.luks.passphrase | string | length) > 0
fail_msg: "system.luks.method must be 'tpm2' or 'keyfile', and luks.passphrase must be set when LUKS is enabled."
quiet: true
no_log: true

View File

@@ -1,22 +1,48 @@
---
partitioning_btrfs_compress_opt: "{{ 'compress=zstd:15' if system_cfg.features.zstd.enabled | bool else '' }}"
# Partition separator: 'p' for NVMe/mmcblk (device path ends in digit), empty for SCSI/virtio.
# Examples: /dev/sda → /dev/sda1, /dev/nvme0n1 → /dev/nvme0n1p1
partitioning_part_sep: "{{ 'p' if (install_drive | default('') | regex_search('\\d$')) else '' }}"
partitioning_boot_partition_suffix: 1
partitioning_main_partition_suffix: 2
partitioning_efi_size_mib: 512
partitioning_efi_start_mib: 1
partitioning_efi_end_mib: "{{ (partitioning_efi_start_mib | int) + (partitioning_efi_size_mib | int) }}"
partitioning_boot_size_mib: 1024
partitioning_vg_name: sys
partitioning_use_full_disk: true
# LVM logical volume sizing
partitioning_lvm_var_gb: 2
partitioning_lvm_var_log_gb: 2
partitioning_lvm_var_log_audit_gb: 1.5
# Disk overhead subtracted from available space in swap/home calculations
partitioning_disk_overhead_gb: 20
# CIS-required reserved space for /var, /var/log, /var/log/audit, /home
partitioning_cis_reserved_gb: 7.5
# Home allocation: percentage of (disk - overhead), bounded by min/max
partitioning_home_allocation_pct: 0.1
partitioning_home_min_gb: 2
partitioning_home_max_gb: 20
# Btrfs home quota (applied when CIS is enabled)
partitioning_btrfs_home_quota: 2G
partitioning_separate_boot: >-
{{
(system_cfg.luks.enabled | bool)
and (os not in ['archlinux'])
(
(system_cfg.luks.enabled | bool)
or (system_cfg.filesystem != 'btrfs')
)
and ((os | default('')) not in ['archlinux'])
}}
partitioning_boot_fs_fstype: >-
{{
system_cfg.filesystem
if system_cfg.filesystem != 'btrfs'
else ('xfs' if is_rhel else 'ext4')
else ('xfs' if (is_rhel | default(false) | bool) else 'ext4')
}}
partitioning_boot_fs_partition_suffix: >-
{{
@@ -34,7 +60,7 @@ partitioning_efi_mountpoint: >-
if (partitioning_separate_boot | bool)
else (
'/boot/efi'
if is_rhel or (os in ['ubuntu', 'ubuntu-lts'] or (os == 'debian' and (os_version | string) in ['11', '12', '13']))
if (is_rhel | default(false) | bool) or ((os | default('')) in ['ubuntu', 'ubuntu-lts'] or ((os | default('')) == 'debian' and (os_version | default('') | string) in ['11', '12', '13']))
else '/boot'
)
}}
@@ -90,12 +116,12 @@ partitioning_grub_enable_cryptodisk: >-
and not (partitioning_separate_boot | bool)
and (partitioning_efi_mountpoint == '/boot/efi')
}}
partitioning_luks_device: "{{ install_drive ~ (partitioning_root_partition_suffix | string) }}"
partitioning_luks_device: "{{ install_drive ~ partitioning_part_sep ~ (partitioning_root_partition_suffix | string) }}"
partitioning_root_device: >-
{{
'/dev/mapper/' + system_cfg.luks.mapper
if (system_cfg.luks.enabled | bool)
else install_drive ~ (partitioning_root_partition_suffix | string)
else install_drive ~ partitioning_part_sep ~ (partitioning_root_partition_suffix | string)
}}
partitioning_disk_size_gb: >-
{{
@@ -129,6 +155,6 @@ partitioning_swap_size_gb: >-
((partitioning_memory_mb / 1024) >= 16.0)
| ternary(
(partitioning_memory_mb / 2048) | int,
[partitioning_memory_mb / 1024, 4.0] | max | int
[partitioning_memory_mb / 1024, 2.0] | max | int
)
}}

View File

@@ -0,0 +1,145 @@
---
- name: Create filesystems
block:
- name: Create FAT32 filesystem in boot partition
community.general.filesystem:
dev: "{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_partition_suffix }}"
fstype: vfat
opts: -F32 -n BOOT
force: true
- name: Create filesystem for /boot partition
when: partitioning_separate_boot | bool
community.general.filesystem:
dev: "{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
fstype: "{{ partitioning_boot_fs_fstype }}"
opts: "{{ '-m bigtime=0 -i nrext64=0,exchange=0 -n parent=0' if (is_rhel | bool and partitioning_boot_fs_fstype == 'xfs') else omit }}"
force: true
- name: Remove unsupported ext4 features from /boot
when:
- partitioning_separate_boot | bool
- partitioning_boot_fs_fstype == 'ext4'
- os in ['almalinux', 'rocky', 'rhel'] or (os == 'debian' and (os_version | string) == '11')
ansible.builtin.command: >-
tune2fs -O "^orphan_file,^metadata_csum_seed"
"{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}"
changed_when: false
- name: Create swap filesystem
when:
- system_cfg.filesystem != 'btrfs'
- system_cfg.features.swap.enabled | bool
community.general.filesystem:
fstype: swap
dev: /dev/{{ partitioning_vg_name }}/swap
- name: Create filesystem
ansible.builtin.include_tasks: "{{ system_cfg.filesystem }}.yml"
- name: Get UUID for boot filesystem
ansible.builtin.command: blkid -s UUID -o value '{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_partition_suffix }}'
register: partitioning_boot_uuid
changed_when: false
failed_when: partitioning_boot_uuid.rc != 0 or (partitioning_boot_uuid.stdout | trim | length) == 0
- name: Get UUID for /boot filesystem
when: partitioning_separate_boot | bool
ansible.builtin.command: >-
blkid -s UUID -o value '{{ install_drive }}{{ partitioning_part_sep }}{{ partitioning_boot_fs_partition_suffix }}'
register: partitioning_boot_fs_uuid
changed_when: false
failed_when: partitioning_boot_fs_uuid.rc != 0 or (partitioning_boot_fs_uuid.stdout | trim | length) == 0
- name: Get UUID for main filesystem
ansible.builtin.command: blkid -s UUID -o value '{{ partitioning_root_device }}'
register: partitioning_main_uuid
changed_when: false
failed_when: partitioning_main_uuid.rc != 0 or (partitioning_main_uuid.stdout | trim | length) == 0
- name: Get UUID for LVM root filesystem
when: system_cfg.filesystem != 'btrfs'
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/root
register: partitioning_uuid_root_result
changed_when: false
failed_when: partitioning_uuid_root_result.rc != 0 or (partitioning_uuid_root_result.stdout | trim | length) == 0
- name: Get UUID for LVM swap filesystem
when:
- system_cfg.filesystem != 'btrfs'
- system_cfg.features.swap.enabled | bool
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/swap
register: partitioning_uuid_swap_result
changed_when: false
failed_when: partitioning_uuid_swap_result.rc != 0 or (partitioning_uuid_swap_result.stdout | trim | length) == 0
- name: Get UUID for LVM home filesystem
when:
- system_cfg.filesystem != 'btrfs'
- system_cfg.features.cis.enabled | bool
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/home
register: partitioning_uuid_home_result
changed_when: false
failed_when: partitioning_uuid_home_result.rc != 0 or (partitioning_uuid_home_result.stdout | trim | length) == 0
- name: Get UUID for LVM var filesystem
when:
- system_cfg.filesystem != 'btrfs'
- system_cfg.features.cis.enabled | bool
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var
register: partitioning_uuid_var_result
changed_when: false
failed_when: partitioning_uuid_var_result.rc != 0 or (partitioning_uuid_var_result.stdout | trim | length) == 0
- name: Get UUID for LVM var_log filesystem
when:
- system_cfg.filesystem != 'btrfs'
- system_cfg.features.cis.enabled | bool
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var_log
register: partitioning_uuid_var_log_result
changed_when: false
failed_when: partitioning_uuid_var_log_result.rc != 0 or (partitioning_uuid_var_log_result.stdout | trim | length) == 0
- name: Get UUID for LVM var_log_audit filesystem
when:
- system_cfg.filesystem != 'btrfs'
- system_cfg.features.cis.enabled | bool
ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var_log_audit
register: partitioning_uuid_var_log_audit_result
changed_when: false
failed_when: partitioning_uuid_var_log_audit_result.rc != 0 or (partitioning_uuid_var_log_audit_result.stdout | trim | length) == 0
- name: Assign UUIDs to Variables
when: system_cfg.filesystem != 'btrfs'
ansible.builtin.set_fact:
partitioning_uuid_root: "{{ partitioning_uuid_root_result.stdout_lines | default([]) }}"
partitioning_uuid_swap: >-
{{
partitioning_uuid_swap_result.stdout_lines | default([])
if system_cfg.features.swap.enabled | bool
else []
}}
partitioning_uuid_home: >-
{{
partitioning_uuid_home_result.stdout_lines | default([])
if system_cfg.features.cis.enabled | bool
else []
}}
partitioning_uuid_var: >-
{{
partitioning_uuid_var_result.stdout_lines | default([])
if system_cfg.features.cis.enabled | bool
else []
}}
partitioning_uuid_var_log: >-
{{
partitioning_uuid_var_log_result.stdout_lines | default([])
if system_cfg.features.cis.enabled | bool
else []
}}
partitioning_uuid_var_log_audit: >-
{{
partitioning_uuid_var_log_audit_result.stdout_lines | default([])
if system_cfg.features.cis.enabled | bool
else []
}}

View File

@@ -0,0 +1,192 @@
---
# LVM Sizing Algorithm
# ====================
# Sizes are computed from disk_size_gb, memory_mb, and feature flags.
#
# Swap sizing:
# - RAM >= 16 GB → swap = RAM/2 (in GB)
# - RAM < 16 GB → swap = max(RAM_GB, 2)
# - Capped to: min(target, 4 + max(disk - overhead, 0))
# - Further capped to: max available after subtracting reserved + CIS + extent reserve + 4 GB buffer
#
# Root sizing:
# - Full-disk mode (default): disk - reserved - swap - extent_reserve - (CIS volumes if enabled)
# - Partial mode: tiered — <4 GB available → 4 GB, 4-12 GB → all available, >12 GB → 40% of disk
#
# CIS volumes (only when CIS enabled):
# - /home: max(min(home_raw, home_max), home_min) where home_raw = (disk - overhead) * 10%
# - /var: 2 GB, /var/log: 2 GB, /var/log/audit: 1.5 GB
#
# Extent reserve: 10 extents * 4 MiB = ~0.04 GB (prevents VG overflow)
- name: Create LVM logical volumes
when: system_cfg.filesystem != 'btrfs'
block:
- name: Create LVM volume group
community.general.lvg:
vg: "{{ partitioning_vg_name }}"
pvs: "{{ partitioning_root_device }}"
- name: Create LVM logical volumes
when:
- system_cfg.features.cis.enabled | bool or item.lv not in ['home', 'var', 'var_log', 'var_log_audit']
- system_cfg.features.swap.enabled | bool or item.lv != 'swap'
vars:
partitioning_lvm_extent_reserve_count: 10
partitioning_lvm_extent_size_mib: 4
partitioning_lvm_extent_reserve_gb: >-
{{
(
(partitioning_lvm_extent_reserve_count | float)
* (partitioning_lvm_extent_size_mib | float)
/ 1024
) | round(2, 'ceil')
}}
partitioning_lvm_swap_target_gb: >-
{{
(
((partitioning_memory_mb | float / 1024) >= 16.0)
| ternary(
(partitioning_memory_mb | float / 2048),
[(partitioning_memory_mb | float / 1024), 2] | max | float
)
)
if system_cfg.features.swap.enabled | bool
else 0
}}
partitioning_lvm_swap_cap_gb: >-
{{
(
4
+ [
(partitioning_disk_size_gb | float) - (partitioning_disk_overhead_gb | float),
0
] | max
)
if system_cfg.features.swap.enabled | bool
else 0
}}
partitioning_lvm_swap_target_limited_gb: >-
{{
(
[
partitioning_lvm_swap_target_gb,
partitioning_lvm_swap_cap_gb
] | min
)
if system_cfg.features.swap.enabled | bool
else 0
}}
partitioning_lvm_swap_max_gb: >-
{{
(
[
(
(partitioning_disk_size_gb | float)
- (partitioning_reserved_gb | float)
- (system_cfg.features.cis.enabled | bool | ternary(partitioning_cis_reserved_gb | float, 0))
- partitioning_lvm_extent_reserve_gb
- 4
),
0
] | max
)
if system_cfg.features.swap.enabled | bool
else 0
}}
partitioning_lvm_available_gb: >-
{{
(
(partitioning_disk_size_gb | float)
- (partitioning_reserved_gb | float)
- (system_cfg.features.cis.enabled | bool | ternary(partitioning_cis_reserved_gb | float, 0))
- partitioning_lvm_extent_reserve_gb
- partitioning_lvm_swap_target_limited_gb
) | float
}}
partitioning_lvm_home_raw_gb: >-
{{
((partitioning_disk_size_gb | float) - (partitioning_disk_overhead_gb | float))
* (partitioning_home_allocation_pct | float)
}}
partitioning_lvm_home_gb: >-
{{
[
[(partitioning_lvm_home_raw_gb | float), (partitioning_home_min_gb | float)] | max,
(partitioning_home_max_gb | float)
] | min
}}
partitioning_lvm_root_default_gb: >-
{{
[
(
((partitioning_lvm_available_gb | float) < 4)
| ternary(
4,
(
((partitioning_lvm_available_gb | float) > 12)
| ternary(
((partitioning_disk_size_gb | float) * 0.4)
| round(0, 'ceil'),
partitioning_lvm_available_gb
)
)
)
),
4
] | max
}}
partitioning_lvm_swap_gb: >-
{{
(
[
partitioning_lvm_swap_target_limited_gb,
partitioning_lvm_swap_max_gb
] | min | round(2, 'floor')
)
if system_cfg.features.swap.enabled | bool
else 0
}}
partitioning_lvm_root_full_gb: >-
{{
[
(
(partitioning_disk_size_gb | float)
- (partitioning_reserved_gb | float)
- (partitioning_lvm_swap_gb | float)
- partitioning_lvm_extent_reserve_gb
- (
(partitioning_lvm_home_gb | float)
+ (partitioning_lvm_var_gb | float)
+ (partitioning_lvm_var_log_gb | float)
+ (partitioning_lvm_var_log_audit_gb | float)
if system_cfg.features.cis.enabled | bool
else 0
)
),
4
] | max | round(2, 'floor')
}}
partitioning_lvm_root_gb: >-
{{
partitioning_lvm_root_full_gb
if partitioning_use_full_disk | bool
else partitioning_lvm_root_default_gb
}}
community.general.lvol:
vg: "{{ partitioning_vg_name }}"
lv: "{{ item.lv }}"
size: "{{ item.size }}"
state: present
loop:
- lv: root
size: "{{ partitioning_lvm_root_gb | string + 'G' }}"
- lv: swap
size: "{{ partitioning_lvm_swap_gb | string + 'G' }}"
- lv: home
size: "{{ partitioning_lvm_home_gb | string + 'G' }}"
- { lv: var, size: "{{ partitioning_lvm_var_gb }}G" }
- { lv: var_log, size: "{{ partitioning_lvm_var_log_gb }}G" }
- { lv: var_log_audit, size: "{{ partitioning_lvm_var_log_audit_gb }}G" }
loop_control:
label: "{{ item.lv }}"

View File

@@ -0,0 +1,116 @@
---
- name: Partition install drive
block:
- name: Prepare partitions
block:
- name: Disable swap
ansible.builtin.command: swapoff -a
register: partitioning_swapoff_result
changed_when: partitioning_swapoff_result.rc == 0
failed_when: false
- name: Find mounts under /mnt
ansible.builtin.command: findmnt -R /mnt -n -o TARGET
register: partitioning_mounted_paths
changed_when: false
failed_when: false
- name: Unmount /mnt mounts
when: partitioning_mounted_paths.stdout_lines | length > 0
ansible.posix.mount:
path: "{{ item }}"
state: unmounted
loop: "{{ partitioning_mounted_paths.stdout_lines | reverse }}"
loop_control:
label: "{{ item }}"
failed_when: false
- name: Remove LVM volume group
community.general.lvg:
vg: "{{ partitioning_vg_name }}"
state: absent
force: true
failed_when: false
- name: Close LUKS mapper
when: system_cfg.luks.enabled | bool
community.crypto.luks_device:
name: "{{ system_cfg.luks.mapper }}"
state: closed
failed_when: false
- name: Remove LUKS mapper device
when: system_cfg.luks.enabled | bool
ansible.builtin.command: >-
dmsetup remove --force --retry {{ system_cfg.luks.mapper }}
register: partitioning_dmsetup_remove
changed_when: partitioning_dmsetup_remove.rc == 0
failed_when: false
- name: Remove LUKS signatures
when: system_cfg.luks.enabled | bool
community.crypto.luks_device:
device: "{{ partitioning_luks_device }}"
state: absent
failed_when: false
- name: Wipe filesystem signatures
ansible.builtin.shell: >-
find /dev -wholename "{{ install_drive }}*" -exec wipefs --force --all {} \;
register: partitioning_wipefs_result
changed_when: partitioning_wipefs_result.rc == 0
failed_when: false
- name: Refresh kernel partition table
ansible.builtin.command: "{{ item }}"
loop:
- "partprobe {{ install_drive }}"
- "udevadm settle"
changed_when: false
failed_when: false
- name: Define partitions
block:
- name: Create partition layout
community.general.parted:
device: "{{ install_drive }}"
label: gpt
number: "{{ item.number }}"
part_end: "{{ item.part_end | default(omit) }}"
part_start: "{{ item.part_start | default(omit) }}"
name: "{{ item.name }}"
flags: "{{ item.flags | default(omit) }}"
state: present
loop: "{{ partitioning_layout }}"
loop_control:
label: "{{ item.name }}"
rescue:
- name: Refresh kernel partition table after failure
ansible.builtin.command: "{{ item }}"
loop:
- "partprobe {{ install_drive }}"
- "udevadm settle"
changed_when: false
failed_when: false
- name: Retry partition layout
community.general.parted:
device: "{{ install_drive }}"
label: gpt
number: "{{ item.number }}"
part_end: "{{ item.part_end | default(omit) }}"
part_start: "{{ item.part_start | default(omit) }}"
name: "{{ item.name }}"
flags: "{{ item.flags | default(omit) }}"
state: present
loop: "{{ partitioning_layout }}"
loop_control:
label: "{{ item.name }}"
- name: Settle partition table
ansible.builtin.command: "{{ item }}"
loop:
- "partprobe {{ install_drive }}"
- "udevadm settle"
changed_when: false
failed_when: false

View File

@@ -0,0 +1,38 @@
---
- name: Detect system memory for swap sizing
when:
- system_cfg.features.swap.enabled | bool
- partitioning_vm_memory is not defined or (partitioning_vm_memory | float) <= 0
- (system_cfg.memory | default(0) | float) <= 0
block:
- name: Read system memory
ansible.builtin.command: awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo
register: partitioning_memtotal_mb
changed_when: false
failed_when: false
- name: Set partitioning vm memory default
ansible.builtin.set_fact:
partitioning_vm_memory: "{{ (partitioning_memtotal_mb.stdout | default('4096') | int) | float }}"
- name: Set partitioning vm_size for physical installs
when:
- system_cfg.type == "physical"
- partitioning_vm_size is not defined or (partitioning_vm_size | float) <= 0
- install_drive | length > 0
block:
- name: Detect install drive size
ansible.builtin.command: "lsblk -b -dn -o SIZE {{ install_drive }}"
register: partitioning_disk_size_bytes
changed_when: false
- name: Set partitioning vm_size from install drive size
when:
- partitioning_disk_size_bytes.stdout is defined
- (partitioning_disk_size_bytes.stdout | trim | length) > 0
ansible.builtin.set_fact:
partitioning_vm_size: >-
{{
(partitioning_disk_size_bytes.stdout | trim | int / 1024 / 1024 / 1024)
| round(2, 'floor')
}}

View File

@@ -0,0 +1,129 @@
---
- name: Mount filesystems
block:
# CIS mode: mount all paths (separate partitions for /home, /var, etc.)
# Non-CIS btrfs: only mount subvolume paths (/home, /var/log, /var/cache/pacman/pkg)
# Non-CIS LVM: skip CIS-only paths (/home, /var, /var/log, /var/log/audit, /var/cache/pacman/pkg)
- name: Mount filesystems and subvolumes
when:
- >-
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'])
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')
- system_cfg.features.swap.enabled | bool or item.path != '/swap'
ansible.posix.mount:
path: /mnt{{ item.path }}
src: "{{ 'UUID=' + (partitioning_main_uuid.stdout if system_cfg.filesystem == 'btrfs' else item.uuid) }}"
fstype: "{{ system_cfg.filesystem }}"
opts: "{{ item.opts }}"
state: mounted
loop:
# ssd: no-op on kernels 5.15+ (btrfs auto-detects); kept for older kernel compat
- path: ""
uuid: "{{ partitioning_uuid_root[0] | default(omit) }}"
opts: >-
{{
'defaults'
if system_cfg.filesystem != 'btrfs'
else [
'rw', 'relatime', partitioning_btrfs_compress_opt, 'ssd', 'space_cache=v2',
'discard=async', 'subvol=@'
] | reject('equalto', '') | join(',')
}}
- path: /swap
opts: >-
{{
[
'rw', 'nosuid', 'nodev', 'relatime', partitioning_btrfs_compress_opt, 'ssd',
'space_cache=v2', 'discard=async', 'subvol=@swap'
] | reject('equalto', '') | join(',')
}}
- path: /home
uuid: "{{ partitioning_uuid_home[0] | default(omit) }}"
opts: >-
{{
'defaults,nosuid,nodev'
if system_cfg.filesystem != 'btrfs'
else [
'rw', 'nosuid', 'nodev', 'relatime', partitioning_btrfs_compress_opt, 'ssd',
'space_cache=v2', 'discard=async', 'subvol=@home'
] | reject('equalto', '') | join(',')
}}
- path: /var
uuid: "{{ partitioning_uuid_var[0] | default(omit) }}"
opts: >-
{{
'defaults,nosuid,nodev'
if system_cfg.filesystem != 'btrfs'
else [
'rw', 'nosuid', 'nodev', 'relatime', partitioning_btrfs_compress_opt, 'ssd',
'space_cache=v2', 'discard=async', 'subvol=@var'
] | reject('equalto', '') | join(',')
}}
- path: /var/log
uuid: "{{ partitioning_uuid_var_log[0] | default(omit) }}"
opts: >-
{{
'defaults,nosuid,nodev,noexec'
if system_cfg.filesystem != 'btrfs'
else [
'rw', 'nosuid', 'nodev', 'noexec', 'relatime', partitioning_btrfs_compress_opt,
'ssd', 'space_cache=v2', 'discard=async', 'subvol=@var_log'
] | reject('equalto', '') | join(',')
}}
- path: /var/cache/pacman/pkg
uuid: "{{ partitioning_uuid_root | default([]) | first | default(omit) }}"
opts: >-
{{
'defaults,nosuid,nodev,noexec'
if system_cfg.filesystem != 'btrfs'
else [
'rw', 'nosuid', 'nodev', 'noexec', 'relatime', partitioning_btrfs_compress_opt,
'ssd', 'space_cache=v2', 'discard=async', 'subvol=@pkg'
] | reject('equalto', '') | join(',')
}}
- path: /var/log/audit
uuid: "{{ partitioning_uuid_var_log_audit[0] | default(omit) }}"
opts: >-
{{
'defaults,nosuid,nodev,noexec'
if system_cfg.filesystem != 'btrfs'
else [
'rw', 'nosuid', 'nodev', 'noexec', 'relatime', partitioning_btrfs_compress_opt,
'ssd', 'space_cache=v2', 'discard=async', 'subvol=@var_log_audit'
] | reject('equalto', '') | join(',')
}}
loop_control:
label: "{{ item.path }}"
- name: Mount /boot filesystem
when: partitioning_separate_boot | bool
ansible.posix.mount:
path: /mnt/boot
src: "UUID={{ partitioning_boot_fs_uuid.stdout }}"
fstype: "{{ partitioning_boot_fs_fstype }}"
opts: defaults
state: mounted
- name: Mount boot filesystem
ansible.posix.mount:
path: "/mnt{{ partitioning_efi_mountpoint }}"
src: UUID={{ partitioning_boot_uuid.stdout }}
fstype: vfat
state: mounted
- name: Activate swap
when: system_cfg.features.swap.enabled | bool
vars:
partitioning_swap_cmd: >-
{{ 'swapon /mnt/swap/swapfile' if system_cfg.filesystem == 'btrfs' else 'swapon -U ' + partitioning_uuid_swap[0] }}
ansible.builtin.command: "{{ partitioning_swap_cmd }}"
register: partitioning_swap_activate_result
# swapon returns 255 if swap is already active
failed_when: partitioning_swap_activate_result.rc not in [0, 255]
changed_when: partitioning_swap_activate_result.rc == 0

View File

@@ -0,0 +1,89 @@
---
- name: Configure LUKS encryption
when: system_cfg.luks.enabled | bool
block:
- name: Validate LUKS passphrase
ansible.builtin.assert:
that:
- (system_cfg.luks.passphrase | string | length) > 0
fail_msg: system.luks.passphrase must be set when LUKS is enabled.
no_log: true
- name: Ensure LUKS container exists
community.crypto.luks_device:
device: "{{ partitioning_luks_device }}"
state: present
type: "{{ system_cfg.luks.type }}"
cipher: "{{ system_cfg.luks.cipher }}"
hash: "{{ system_cfg.luks.hash }}"
keysize: "{{ system_cfg.luks.bits }}"
pbkdf:
algorithm: "{{ system_cfg.luks.pbkdf }}"
iteration_time: "{{ (system_cfg.luks.iter | float) / 1000 }}"
passphrase: "{{ system_cfg.luks.passphrase | string }}"
no_log: true
- name: Force-close LUKS mapper
community.crypto.luks_device:
name: "{{ system_cfg.luks.mapper }}"
state: closed
failed_when: false
- name: Force-remove LUKS mapper device
ansible.builtin.command: >-
dmsetup remove --force --retry {{ system_cfg.luks.mapper }}
register: partitioning_dmsetup_remove_after_format
changed_when: partitioning_dmsetup_remove_after_format.rc == 0
failed_when: false
- name: Settle udev after removing LUKS mapper
ansible.builtin.command: udevadm settle
changed_when: false
failed_when: false
- name: Ensure LUKS mapper is opened
block:
- name: Open LUKS device
community.crypto.luks_device:
device: "{{ partitioning_luks_device }}"
state: opened
name: "{{ system_cfg.luks.mapper }}"
passphrase: "{{ system_cfg.luks.passphrase | string }}"
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
no_log: true
rescue:
- name: Force-close stale LUKS mapper
community.crypto.luks_device:
name: "{{ system_cfg.luks.mapper }}"
state: closed
failed_when: false
- name: Force-remove stale LUKS mapper device
ansible.builtin.command: >-
dmsetup remove --force --retry {{ system_cfg.luks.mapper }}
register: partitioning_dmsetup_remove_retry
changed_when: partitioning_dmsetup_remove_retry.rc == 0
failed_when: false
- name: Settle udev after removing stale LUKS mapper
ansible.builtin.command: udevadm settle
changed_when: false
failed_when: false
- name: Retry opening LUKS device
community.crypto.luks_device:
device: "{{ partitioning_luks_device }}"
state: opened
name: "{{ system_cfg.luks.mapper }}"
passphrase: "{{ system_cfg.luks.passphrase | string }}"
allow_discards: "{{ 'discard' in (system_cfg.luks.options | lower) }}"
no_log: true
- name: Get LUKS UUID
ansible.builtin.command: "cryptsetup luksUUID {{ partitioning_luks_device }}"
register: partitioning_luks_uuid_result
changed_when: false
- name: Store LUKS UUID
ansible.builtin.set_fact:
partitioning_luks_uuid: "{{ partitioning_luks_uuid_result.stdout | trim }}"

View File

@@ -37,11 +37,11 @@
- name: Enable quotas on Btrfs filesystem
ansible.builtin.command: btrfs quota enable /mnt
register: partitioning_btrfs_quota_result
changed_when: false
changed_when: partitioning_btrfs_quota_result.rc == 0
- name: Make root subvolumes
when:
- system_cfg.features.cis.enabled or item.subvol not in ['var_log_audit']
- system_cfg.features.cis.enabled | bool or item.subvol not in ['var_log_audit']
- system_cfg.features.swap.enabled | bool or item.subvol != 'swap'
ansible.builtin.command: btrfs su cr /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
args:
@@ -54,15 +54,18 @@
- { subvol: pkg }
- { subvol: var_log }
- { subvol: var_log_audit }
register: partitioning_btrfs_subvol_result
loop_control:
label: "{{ item.subvol }}"
- name: Set quotas for subvolumes
when: system_cfg.features.cis.enabled
when: system_cfg.features.cis.enabled | bool
ansible.builtin.command: btrfs qgroup limit {{ item.quota }} /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
loop:
- { subvol: home, quota: 2G }
- { subvol: home, quota: "{{ partitioning_btrfs_home_quota }}" }
loop_control:
label: "{{ item.subvol }}"
register: partitioning_btrfs_qgroup_result
changed_when: false
changed_when: partitioning_btrfs_qgroup_result.rc == 0
- name: Create a Btrfs swap file
when: system_cfg.features.swap.enabled | bool
@@ -70,7 +73,6 @@
btrfs filesystem mkswapfile --size {{ partitioning_swap_size_gb }}g --uuid clear /mnt/@swap/swapfile
args:
creates: /mnt/@swap/swapfile
register: partitioning_btrfs_swap_result
- name: Unmount Partition
ansible.posix.mount:

View File

@@ -1,8 +1,8 @@
---
- name: Create and format ext4 logical volumes
when: system_cfg.features.cis.enabled or item.lv not in ['home', 'var', 'var_log', 'var_log_audit']
when: system_cfg.features.cis.enabled | bool or item.lv not in ['home', 'var', 'var_log', 'var_log_audit']
community.general.filesystem:
dev: /dev/sys/{{ item.lv }}
dev: /dev/{{ partitioning_vg_name }}/{{ item.lv }}
fstype: ext4
force: true
loop:
@@ -11,17 +11,21 @@
- { lv: var }
- { lv: var_log }
- { lv: var_log_audit }
loop_control:
label: "{{ item.lv }}"
- name: Remove Unsupported features for older Systems
when: >
(os in ['almalinux', 'rocky', 'rhel'] or (os == 'debian' and (os_version | string) == '11'))
and (system_cfg.features.cis.enabled or item.lv not in ['home', 'var', 'var_log', 'var_log_audit'])
ansible.builtin.command: tune2fs -O "^orphan_file,^metadata_csum_seed" "/dev/sys/{{ item.lv }}"
and (system_cfg.features.cis.enabled | bool or item.lv not in ['home', 'var', 'var_log', 'var_log_audit'])
ansible.builtin.command: tune2fs -O "^orphan_file,^metadata_csum_seed" "/dev/{{ partitioning_vg_name }}/{{ item.lv }}"
loop:
- { lv: root }
- { lv: home }
- { lv: var }
- { lv: var_log }
- { lv: var_log_audit }
loop_control:
label: "{{ item.lv }}"
register: partitioning_ext4_tune_result
changed_when: partitioning_ext4_tune_result.rc == 0

Some files were not shown because too many files have changed in this diff Show More