Compare commits

...

56 Commits

Author SHA1 Message Date
07492b5b57 refactor(cleanup): add configurable verify_boot, boot_timeout, and remove_on_failure defaults 2026-02-20 23:02:24 +01:00
14913bcd3d refactor: move playbook-root templates into their respective roles 2026-02-20 23:01:38 +01:00
041650c287 refactor: add loop_control labels to dict-based loops across all roles 2026-02-20 23:00:53 +01:00
a63ffbc731 refactor(partitioning): move btrfs home quota to configurable default 2026-02-20 22:55:37 +01:00
9d2f1cc5bd fix(environment): detect RHEL ISO device dynamically instead of hardcoded /dev/sr paths 2026-02-20 22:54:42 +01:00
f72f9feb9a refactor(global_defaults): split system.yml into composable normalization stages 2026-02-20 22:54:05 +01:00
417737f904 refactor(global_defaults): extract OS family lists to single source of truth 2026-02-20 22:52:55 +01:00
a06c2ebdcf fix(partitioning): add failed_when to all blkid commands to catch empty UUIDs 2026-02-20 22:52:18 +01:00
e174ecda42 fix(partitioning): add default fallbacks for is_rhel, os, os_version in defaults 2026-02-20 22:51:37 +01:00
5246a905bb fix(virtualization): use hostname variable instead of hardcoded archiso in cloud-user-data 2026-02-20 22:51:32 +01:00
d00d84b69c fix(virtualization): avoid no-handler lint finding in xen VM created tracking 2026-02-20 22:29:03 +01:00
4dafa8c596 fix(partitioning): fix line length violation in home size calculation 2026-02-20 22:28:58 +01:00
53584b8730 fix(configuration): add pipefail to root password shell pipe 2026-02-20 22:28:54 +01:00
ce40468b77 fix(bootstrap): use release map for ubuntu version detection 2026-02-20 22:27:46 +01:00
4b4fab3c33 chore: add .yamllint matching main project conventions 2026-02-20 22:27:31 +01:00
db2fab5e7d fix(configuration): use chpasswd for root password and separate shell setting 2026-02-20 22:27:17 +01:00
42be0a5919 fix(configuration): add explicit LUKS auto-decrypt fallback state tracking and logging 2026-02-20 22:26:47 +01:00
17400fa6ff refactor(partitioning): externalize hardcoded LVM and disk sizing constants to defaults 2026-02-20 22:26:23 +01:00
deb14d2c94 fix(virtualization): add xen VM existence check and improve changed_when 2026-02-20 22:25:10 +01:00
65c5b1029b fix(cis): add pipefail to sshd version detection and define binary defaults 2026-02-20 22:24:14 +01:00
a1fbb7c21d feat(cleanup): gate RHEL ISO disk and fstab handling on rhel_repo.source 2026-02-20 21:51:20 +01:00
d076ac8fef feat(global_defaults): add system.features.rhel_repo option (iso|satellite|none) 2026-02-20 21:51:16 +01:00
c82e4afc4d fix(encryption): add warning before silent TPM2-to-keyfile fallback 2026-02-20 21:51:12 +01:00
ac72fdc4a6 fix(partitioning): correct wipefs changed_when to report actual disk modification 2026-02-20 21:51:09 +01:00
b2e050c467 fix(validation): require password for primary user in system.users[0] 2026-02-20 21:51:06 +01:00
914d7dd9d1 fix(system_check): move no_log from block to individual API tasks 2026-02-20 21:51:02 +01:00
21bf8f79e2 fix(cis): make mlkem768x25519-sha256 KexAlgorithm conditional on OpenSSH 9.9+ 2026-02-20 21:50:58 +01:00
38feff4369 fix(cis): use is_rhel for journald config path instead of fedora-only check 2026-02-20 21:50:55 +01:00
404529e8a4 refactor(configuration): add conditional dispatch to task includes 2026-02-20 21:16:52 +01:00
3db18858c3 refactor(cis): move OS-specific binary resolution to vars/main.yml 2026-02-20 21:16:48 +01:00
72a9576abe refactor(configuration): split network.yml into per-init-system dispatch files 2026-02-20 21:16:45 +01:00
462c2c7dfe refactor(bootstrap): restructure conditional package lists to list concatenation 2026-02-20 21:16:40 +01:00
ef8bfeaf84 refactor(configuration): convert services.yml to list-based loop 2026-02-20 21:16:37 +01:00
ba6be037ac refactor(virt): adopt module_defaults for hypervisor credentials 2026-02-20 21:16:33 +01:00
5ca1c7f570 refactor(cleanup): restructure dispatch to use hypervisor_type include 2026-02-20 21:16:28 +01:00
cd8e477534 refactor(partitioning): extract VG name to defaults variable 2026-02-20 21:16:25 +01:00
c439e9741e fix(configuration): remove trailing blank line from extras.yml 2026-02-20 20:20:33 +01:00
0a5c70e49f docs(environment): document RPM GPG policy relaxation 2026-02-20 20:19:57 +01:00
19f2c9efe2 chore(bootstrap): align ansible.cfg with main project settings 2026-02-20 20:19:46 +01:00
230c74fd9b feat(system_check): add safety check for physical installs 2026-02-20 20:19:37 +01:00
a2c19e2e49 fix(cleanup): fix vmware CD-ROM omit fragility and add cross-role defaults 2026-02-20 20:19:25 +01:00
9f9a4b38b8 fix(virtualization): add XML safety attributes and switch xen to virtio 2026-02-20 20:18:49 +01:00
524356cf8d fix(cis): remove deprecated sshd options and update hardening values 2026-02-20 20:17:52 +01:00
a2993212ca fix(configuration): disambiguate BLS task names and clean up misc noise 2026-02-20 20:17:05 +01:00
fba2e5fc94 refactor(configuration): relocate login banner and fix blockinfile markers 2026-02-20 20:16:19 +01:00
cf68a93b45 fix(configuration): use short hostname and allow per-user shell 2026-02-20 20:15:49 +01:00
3000268a0e fix(partitioning): mount extra disks by UUID instead of device path 2026-02-20 20:15:25 +01:00
196c5be67a fix(partitioning): correct LVM swap sizing and harden UUID fallbacks 2026-02-20 20:15:00 +01:00
33bad193b4 fix(configuration): add trailing semicolons to NM keyfile DNS fields 2026-02-20 20:14:06 +01:00
d5277802f7 fix(bootstrap): add missing packages and remove duplicates 2026-02-20 20:13:53 +01:00
28e6cf50d1 fix(bootstrap): add devpts mount and use ephemeral state for RHEL DVD 2026-02-20 20:12:59 +01:00
42cb5071c2 fix(bootstrap): unify resolv.conf to live environment DNS symlink 2026-02-20 20:12:42 +01:00
23a798a63a fix(global_defaults): add no_log to hypervisor tasks and expand validation 2026-02-20 20:11:37 +01:00
5dd84c6b39 fix: configurable OVMF/machine type, routes syntax, package lists, interface names 2026-02-20 18:47:12 +01:00
d0ae20911b fix(cleanup): keep RHEL ISO ide1 attached as local repo 2026-02-20 18:41:40 +01:00
b6d06dd96d fix: deep analysis audit — no_log, resolv.conf, service conflicts, lint 2026-02-20 18:34:59 +01:00
73 changed files with 1367 additions and 1074 deletions

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

View File

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

View File

@@ -16,12 +16,6 @@
register: bootstrap_almalinux_base_result register: bootstrap_almalinux_base_result
changed_when: bootstrap_almalinux_base_result.rc == 0 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 - name: Install extra packages
ansible.builtin.command: >- ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False {{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False

View File

@@ -6,13 +6,6 @@
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ') lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
}} }}
block: 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 packages
ansible.builtin.command: > ansible.builtin.command: >
apk --root /mnt --no-cache add alpine-base apk --root /mnt --no-cache add alpine-base

View File

@@ -16,12 +16,6 @@
register: bootstrap_fedora_base_result register: bootstrap_fedora_base_result
changed_when: bootstrap_fedora_base_result.rc == 0 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 - name: Install extra packages
ansible.builtin.command: >- ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False {{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False

View File

@@ -22,6 +22,9 @@
- { src: proc, path: proc, fstype: proc } - { src: proc, path: proc, fstype: proc }
- { src: sysfs, path: sys, fstype: sysfs } - { src: sysfs, path: sys, fstype: sysfs }
- { src: /dev, path: dev, fstype: none, opts: bind } - { 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 - name: Run OS-specific bootstrap process
vars: vars:
@@ -39,3 +42,10 @@
void: void.yml void: void.yml
bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}" bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}"
ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}" ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}"
- name: 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

@@ -6,13 +6,6 @@
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ') lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
}} }}
block: 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 packages
ansible.builtin.command: > ansible.builtin.command: >
zypper --root /mnt --non-interactive install -t pattern patterns-base-base zypper --root /mnt --non-interactive install -t pattern patterns-base-base

View File

@@ -13,18 +13,6 @@
- bootstrap_result.rc != 0 - bootstrap_result.rc != 0
- "'grub2-common' not in (bootstrap_result.stderr | default(''))" - "'grub2-common' not in (bootstrap_result.stderr | default(''))"
- name: Write resolv.conf into chroot
ansible.builtin.copy:
dest: /mnt/etc/resolv.conf
mode: "0644"
content: |
{% for dns in system_cfg.network.dns.servers %}
nameserver {{ dns }}
{% endfor %}
{% if system_cfg.network.dns.search | default([]) | length > 0 %}
search {{ system_cfg.network.dns.search | join(' ') }}
{% endif %}
- name: Ensure chroot RHEL DVD directory exists - name: Ensure chroot RHEL DVD directory exists
ansible.builtin.file: ansible.builtin.file:
path: /mnt/usr/local/install/redhat/dvd path: /mnt/usr/local/install/redhat/dvd
@@ -37,7 +25,7 @@
path: /mnt/usr/local/install/redhat/dvd path: /mnt/usr/local/install/redhat/dvd
fstype: none fstype: none
opts: bind opts: bind
state: mounted state: ephemeral
- name: Rebuild RPM database inside chroot - name: Rebuild RPM database inside chroot
ansible.builtin.command: "{{ chroot_command }} rpm --rebuilddb" ansible.builtin.command: "{{ chroot_command }} rpm --rebuilddb"

View File

@@ -16,12 +16,6 @@
register: bootstrap_rocky_base_result register: bootstrap_rocky_base_result
changed_when: bootstrap_rocky_base_result.rc == 0 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 - name: Install extra packages
ansible.builtin.command: >- ansible.builtin.command: >-
{{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False {{ chroot_command }} dnf --releasever={{ os_version }} --setopt=install_weak_deps=False

View File

@@ -1,8 +1,11 @@
--- ---
- name: Bootstrap Ubuntu System - name: Bootstrap Ubuntu System
vars: vars:
bootstrap_ubuntu_release: >- # ubuntu = latest non-LTS, ubuntu-lts = latest LTS
{{ 'plucky' if os == 'ubuntu' else 'noble' }} bootstrap_ubuntu_release_map:
ubuntu: plucky
ubuntu-lts: noble
bootstrap_ubuntu_release: "{{ bootstrap_ubuntu_release_map[os] | default('noble') }}"
bootstrap_ubuntu_package_config: >- bootstrap_ubuntu_package_config: >-
{{ {{
lookup('vars', bootstrap_var_key) lookup('vars', bootstrap_var_key)
@@ -47,13 +50,6 @@
register: bootstrap_ubuntu_base_result register: bootstrap_ubuntu_base_result
changed_when: bootstrap_ubuntu_base_result.rc == 0 changed_when: bootstrap_ubuntu_base_result.rc == 0
- name: Ensure chroot has resolv.conf
ansible.builtin.copy:
src: /etc/resolv.conf
dest: /mnt/etc/resolv.conf
remote_src: true
mode: "0644"
- name: Enable universe repository - name: Enable universe repository
ansible.builtin.command: "{{ chroot_command }} sed -i '1s|$| universe|' /etc/apt/sources.list" ansible.builtin.command: "{{ chroot_command }} sed -i '1s|$| universe|' /etc/apt/sources.list"
register: bootstrap_ubuntu_repo_result register: bootstrap_ubuntu_repo_result

View File

@@ -6,13 +6,6 @@
lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ') lookup('vars', bootstrap_var_key) | reject('equalto', '') | join(' ')
}} }}
block: 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 packages
ansible.builtin.command: > 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 void-repo-nonfree base-system

View File

@@ -1,15 +1,18 @@
--- ---
# Common conditional packages shared across distributions. # Common feature-gated packages. Built as a clean list (no empty strings).
# Arch overrides nftables with iptables-nft; SSH package names vary per distro. # Arch overrides nftables iptables-nft; SSH package names vary per distro.
bootstrap_common_conditional: 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 '' }}" (['firewalld'] if system_cfg.features.firewall.backend == 'firewalld' and system_cfg.features.firewall.enabled | bool else [])
- "{{ 'nftables' if system_cfg.features.firewall.toolkit == 'nftables' else '' }}" + (['ufw'] if system_cfg.features.firewall.backend == 'ufw' and system_cfg.features.firewall.enabled | bool else [])
- "{{ 'cryptsetup' if system_cfg.luks.enabled else '' }}" + (['iptables'] if system_cfg.features.firewall.toolkit == 'iptables' and system_cfg.features.firewall.enabled | bool else [])
- "{{ 'tpm2-tools' if system_cfg.luks.enabled else '' }}" + (['nftables'] if system_cfg.features.firewall.toolkit == 'nftables' and system_cfg.features.firewall.enabled | bool else [])
- "{{ 'qemu-guest-agent' if hypervisor_type in ['libvirt', 'proxmox'] else '' }}" + (['cryptsetup', 'tpm2-tools'] if system_cfg.luks.enabled | bool else [])
- "{{ 'open-vm-tools' if hypervisor_type == 'vmware' else '' }}" + (['qemu-guest-agent'] if hypervisor_type in ['libvirt', 'proxmox'] else [])
+ (['open-vm-tools'] if hypervisor_type == 'vmware' else [])
)
}}
bootstrap_rhel_base: >- bootstrap_rhel_base: >-
{{ {{
@@ -20,13 +23,15 @@ bootstrap_rhel_base: >-
+ bootstrap_common_conditional + bootstrap_common_conditional
}} }}
bootstrap_rhel_versioned: bootstrap_rhel_versioned: >-
- grub2 {{
- "{{ 'grub2-efi-x64' if os_version_major | default('') == '8' else 'grub2-efi' }}" ['grub2']
- "{{ 'grub2-tools-extra' if os_version_major | default('') in ['8', '9'] else '' }}" + (['grub2-efi-x64'] if os_version_major | default('') == '8' else ['grub2-efi'])
- "{{ 'python39' if os_version_major | default('') == '8' else 'python' }}" + (['grub2-tools-extra'] if os_version_major | default('') in ['8', '9'] else [])
- "{{ 'kernel' if os_version_major | default('') == '10' else '' }}" + (['python39'] if os_version_major | default('') == '8' else ['python'])
- "{{ 'zram-generator' if os_version_major | default('') in ['9', '10'] else '' }}" + (['kernel'] if os_version_major | default('') == '10' else [])
+ (['zram-generator'] if os_version_major | default('') in ['9', '10'] else [])
}}
bootstrap_rhel: "{{ bootstrap_rhel_base + bootstrap_rhel_versioned }}" bootstrap_rhel: "{{ bootstrap_rhel_base + bootstrap_rhel_versioned }}"
@@ -34,14 +39,14 @@ bootstrap_almalinux: >-
{{ {{
bootstrap_rhel_base bootstrap_rhel_base
+ ['grub2', 'grub2-efi', 'dbus-daemon', 'lrzsz', + ['grub2', 'grub2-efi', 'dbus-daemon', 'lrzsz',
'nfsv4-client-utils', 'nc', 'ppp', 'zram-generator'] 'nfsv4-client-utils', 'nc', 'ppp', 'python3', 'zram-generator']
}} }}
bootstrap_rocky: >- bootstrap_rocky: >-
{{ {{
bootstrap_rhel_base bootstrap_rhel_base
+ ['grub2', 'grub2-efi', 'nfsv4-client-utils', 'nc', 'ppp', + ['grub2', 'grub2-efi', 'nfsv4-client-utils', 'nc', 'ppp',
'telnet', 'util-linux-core', 'wget', 'zram-generator'] 'python3', 'telnet', 'util-linux-core', 'wget', 'zram-generator']
}} }}
bootstrap_fedora: >- bootstrap_fedora: >-
@@ -51,28 +56,18 @@ bootstrap_fedora: >-
'glibc-langpack-de', 'glibc-langpack-en', 'grub2', 'grub2-efi', 'glibc-langpack-de', 'glibc-langpack-en', 'grub2', 'grub2-efi',
'htop', 'iperf3', 'logrotate', 'lrzsz', 'lvm2', 'htop', 'iperf3', 'logrotate', 'lrzsz', 'lvm2',
'nc', 'nfs-utils', 'nfsv4-client-utils', 'polkit', 'ppp', 'nc', 'nfs-utils', 'nfsv4-client-utils', 'polkit', 'ppp',
'ripgrep', 'shim', 'tmux', 'vim-default-editor', 'python3', 'ripgrep', 'shim', 'tmux', 'vim-default-editor',
'wget', 'zoxide', 'zram-generator', 'zstd'] 'wget', 'zoxide', 'zram-generator', 'zstd']
+ bootstrap_common_conditional + bootstrap_common_conditional
}} }}
bootstrap_debian_base_common: bootstrap_debian_base_common: >-
- btrfs-progs {{
- cron ['btrfs-progs', 'cron', 'gnupg', 'grub-efi', 'grub-efi-amd64-signed',
- gnupg 'grub2-common', 'locales', 'logrotate', 'lvm2', 'python3', 'xfsprogs']
- grub-efi + (['cryptsetup-initramfs'] if system_cfg.luks.enabled | bool else [])
- grub-efi-amd64-signed + (['openssh-server'] if system_cfg.features.ssh.enabled | bool else [])
- 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: bootstrap_debian_extra_common:
- apparmor-utils - apparmor-utils
@@ -101,14 +96,16 @@ bootstrap_debian_extra_common:
- wget - wget
- zstd - zstd
bootstrap_debian_extra_versioned: bootstrap_debian_extra_versioned: >-
- linux-image-amd64 {{
- "{{ 'duf' if (os_version | string) not in ['10', '11'] else '' }}" ['linux-image-amd64']
- "{{ 'fastfetch' if (os_version | string) in ['13', 'unstable'] else '' }}" + (['duf'] if (os_version | string) not in ['10', '11'] else [])
- "{{ 'neofetch' if (os_version | string) == '12' else '' }}" + (['fastfetch'] if (os_version | string) in ['13', 'unstable'] else [])
- "{{ 'software-properties-common' if (os_version | string) not in ['13', 'unstable'] else '' }}" + (['neofetch'] if (os_version | string) == '12' else [])
- "{{ 'systemd-zram-generator' if (os_version | string) not in ['10', '11'] else '' }}" + (['software-properties-common'] if (os_version | string) not in ['13', 'unstable'] else [])
- "{{ 'tldr' 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_debian: bootstrap_debian:
base: "{{ bootstrap_debian_base_common }}" base: "{{ bootstrap_debian_base_common }}"
@@ -138,30 +135,36 @@ bootstrap_archlinux: >-
['base', 'btrfs-progs', 'cronie', 'dhcpcd', 'efibootmgr', 'fastfetch', ['base', 'btrfs-progs', 'cronie', 'dhcpcd', 'efibootmgr', 'fastfetch',
'fish', 'fzf', 'grub', 'htop', 'libpwquality', 'linux', 'logrotate', 'fish', 'fzf', 'grub', 'htop', 'libpwquality', 'linux', 'logrotate',
'lrzsz', 'lsof', 'lvm2', 'ncdu', 'networkmanager', 'nfs-utils', 'lrzsz', 'lsof', 'lvm2', 'ncdu', 'networkmanager', 'nfs-utils',
'ppp', 'prometheus-node-exporter', 'python-psycopg2', 'reflector', 'ppp', 'python', 'reflector',
'rsync', 'sudo', 'tldr', 'tmux', 'vim', 'wireguard-tools', 'zram-generator'] 'rsync', 'sudo', 'tldr', 'tmux', 'vim', 'zram-generator']
+ [('openssh' if system_cfg.features.ssh.enabled | bool else '')] + (['openssh'] if system_cfg.features.ssh.enabled | bool else [])
+ [('iptables-nft' if system_cfg.features.firewall.toolkit == 'nftables' 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_common_conditional | reject('equalto', 'nftables') | list)
}} }}
bootstrap_alpine: >- bootstrap_alpine: >-
{{ {{
['alpine-base', 'vim'] ['alpine-base', 'btrfs-progs', 'chrony', 'curl', 'e2fsprogs',
+ [('openssh' if system_cfg.features.ssh.enabled | bool else '')] 'linux-lts', 'logrotate', 'lvm2', 'python3', 'rsync', 'sudo',
'util-linux', 'vim', 'xfsprogs']
+ (['openssh'] if system_cfg.features.ssh.enabled | bool else [])
+ bootstrap_common_conditional + bootstrap_common_conditional
}} }}
bootstrap_opensuse: >- bootstrap_opensuse: >-
{{ {{
['vim'] ['btrfs-progs', 'chrony', 'curl', 'e2fsprogs',
+ [('openssh' if system_cfg.features.ssh.enabled | bool else '')] 'glibc-locale', 'kernel-default', 'logrotate', 'lvm2', 'NetworkManager',
'python3', 'rsync', 'sudo', 'vim', 'xfsprogs']
+ (['openssh'] if system_cfg.features.ssh.enabled | bool else [])
+ bootstrap_common_conditional + bootstrap_common_conditional
}} }}
bootstrap_void: >- bootstrap_void: >-
{{ {{
['vim'] ['btrfs-progs', 'chrony', 'curl', 'dhcpcd', 'e2fsprogs',
+ [('openssh' if system_cfg.features.ssh.enabled | bool else '')] 'logrotate', 'lvm2', 'python3', 'rsync', 'sudo',
'vim', 'xfsprogs']
+ (['openssh'] if system_cfg.features.ssh.enabled | bool else [])
+ bootstrap_common_conditional + bootstrap_common_conditional
}} }}

View File

@@ -1,21 +1,17 @@
--- ---
cis_permission_targets: >- # 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' }}"
{ "path": "/mnt/etc/ssh/sshd_config", "mode": "0600" },
{ "path": "/mnt/etc/cron.hourly", "mode": "0700" }, cis_permission_targets:
{ "path": "/mnt/etc/cron.daily", "mode": "0700" }, - { path: "/mnt/etc/ssh/sshd_config", mode: "0600" }
{ "path": "/mnt/etc/cron.weekly", "mode": "0700" }, - { path: "/mnt/etc/cron.hourly", mode: "0700" }
{ "path": "/mnt/etc/cron.monthly", "mode": "0700" }, - { path: "/mnt/etc/cron.daily", mode: "0700" }
{ "path": "/mnt/etc/cron.d", "mode": "0700" }, - { path: "/mnt/etc/cron.weekly", mode: "0700" }
{ "path": "/mnt/etc/crontab", "mode": "0600" }, - { path: "/mnt/etc/cron.monthly", mode: "0700" }
{ "path": "/mnt/etc/logrotate.conf", "mode": "0644" }, - { path: "/mnt/etc/cron.d", mode: "0700" }
{ "path": "/mnt/usr/sbin/pppd", "mode": "0754" } if os != "rhel" else None, - { path: "/mnt/etc/crontab", mode: "0600" }
{ - { path: "/mnt/etc/logrotate.conf", mode: "0644" }
"path": "/mnt/usr/bin/" - { path: "/mnt/usr/sbin/pppd", mode: "0754" }
+ ("fusermount3" if os in ["archlinux", "fedora", "rocky"] or os == "rhel" or (os == "debian" and (os_version | string) == "12") else "fusermount"), - { path: "/mnt/usr/bin/{{ cis_fusermount_binary }}", mode: "0755" }
"mode": "755" - { path: "/mnt/usr/bin/{{ cis_write_binary }}", mode: "0755" }
},
{ "path": "/mnt/usr/bin/" + ("write.ul" if os == "debian" and (os_version | string) == "11" else "write"), "mode": "755" }
] | reject("none")
}}

View File

@@ -5,11 +5,19 @@
regexp: "^(\\s*)umask\\s+\\d+" regexp: "^(\\s*)umask\\s+\\d+"
line: "umask 027" line: "umask 027"
# Non-RHEL/non-Debian distros: loop evaluates to [] (intentional skip)
- name: Prevent Login to Accounts With Empty Password - name: Prevent Login to Accounts With Empty Password
ansible.builtin.replace: ansible.builtin.replace:
dest: "{{ item }}" dest: "{{ item }}"
regexp: "\\s*nullok" regexp: "\\s*nullok"
replace: "" replace: ""
loop: loop: >-
- /mnt/etc/pam.d/system-auth {{
- /mnt/etc/pam.d/password-auth ['/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,6 @@
--- ---
- name: Configure System Cryptography Policy - 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" ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set DEFAULT:NO-SHA1"
register: cis_crypto_policy_result register: cis_crypto_policy_result
changed_when: "'Setting system-wide crypto-policies to' in cis_crypto_policy_result.stdout" changed_when: "'Setting system-wide crypto-policies to' in cis_crypto_policy_result.stdout"
@@ -9,4 +9,4 @@
ansible.builtin.command: > ansible.builtin.command: >
{{ chroot_command }} systemctl mask {{ 'nftables' if system_cfg.features.firewall.toolkit == 'iptables' else 'iptables' }} bluetooth rpcbind {{ chroot_command }} systemctl mask {{ 'nftables' if system_cfg.features.firewall.toolkit == 'iptables' else 'iptables' }} bluetooth rpcbind
register: cis_mask_services_result 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

@@ -10,6 +10,7 @@
install hfs /bin/false install hfs /bin/false
install hfsplus /bin/false install hfsplus /bin/false
install cramfs /bin/false install cramfs /bin/false
# Note: disabling squashfs breaks snap (Ubuntu). Remove for snap-dependent hosts.
install squashfs /bin/false install squashfs /bin/false
install udf /bin/false install udf /bin/false
install usb-storage /bin/false install usb-storage /bin/false

View File

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

View File

@@ -11,8 +11,8 @@
- { path: /mnt/etc/security/pwquality.conf, content: ocredit = -1 } - { path: /mnt/etc/security/pwquality.conf, content: ocredit = -1 }
- { path: /mnt/etc/security/pwquality.conf, content: lcredit = -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: umask 077 }
- { path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: export TMOUT=3000 } - { path: '/mnt/etc/{{ "bashrc" if is_rhel else "bash.bashrc" }}', content: export TMOUT=900 }
- { path: '/mnt/{{ "usr/lib/systemd/journald.conf" if os == "fedora" else "etc/systemd/journald.conf" }}', content: Storage=persistent } - { path: '/mnt/{{ "usr/lib/systemd/journald.conf" if is_rhel | bool else "etc/systemd/journald.conf" }}', content: Storage=persistent }
- { path: /mnt/etc/sudoers, content: Defaults logfile="/var/log/sudo.log" } - { 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/pam.d/su, content: auth required pam_wheel.so }
- path: >- - path: >-
@@ -44,3 +44,5 @@
password [success=1 default=ignore] pam_unix.so obscure sha512 remember=5 password [success=1 default=ignore] pam_unix.so obscure sha512 remember=5
- { path: /mnt/etc/hosts.deny, content: "ALL: ALL" } - { path: /mnt/etc/hosts.deny, content: "ALL: ALL" }
- { path: /mnt/etc/hosts.allow, content: "sshd: ALL" } - { path: /mnt/etc/hosts.allow, content: "sshd: ALL" }
loop_control:
label: "{{ item.content }}"

View File

@@ -21,7 +21,7 @@
- { option: GSSAPIAuthentication, value: "no" } - { option: GSSAPIAuthentication, value: "no" }
- { option: AllowAgentForwarding, value: "no" } - { option: AllowAgentForwarding, value: "no" }
- { option: AllowTcpForwarding, value: "no" } - { option: AllowTcpForwarding, value: "no" }
- { option: ChallengeResponseAuthentication, value: "no" } - { option: KbdInteractiveAuthentication, value: "no" }
- { option: GatewayPorts, value: "no" } - { option: GatewayPorts, value: "no" }
- { option: X11Forwarding, value: "no" } - { option: X11Forwarding, value: "no" }
- { option: PermitUserEnvironment, value: "no" } - { option: PermitUserEnvironment, value: "no" }
@@ -29,17 +29,34 @@
- { option: ClientAliveCountMax, value: "1" } - { option: ClientAliveCountMax, value: "1" }
- { option: PermitTunnel, value: "no" } - { option: PermitTunnel, value: "no" }
- { option: Banner, value: /etc/issue.net } - { option: Banner, value: /etc/issue.net }
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 - 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: ansible.builtin.blockinfile:
path: /mnt/etc/ssh/sshd_config path: /mnt/etc/ssh/sshd_config
marker: "# {mark} CIS SSH HARDENING" marker: "# {mark} CIS SSH HARDENING"
block: |- block: |-
## CIS Specific ## CIS Specific
Protocol 2
### Ciphers and keying ### ### Ciphers and keying ###
RekeyLimit 512M 6h 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 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 MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
########################### ###########################

View File

@@ -5,9 +5,12 @@
mode: "0644" mode: "0644"
content: | content: |
## CIS Sysctl configurations ## CIS Sysctl configurations
fs.suid_dumpable=0
kernel.dmesg_restrict=1
kernel.yama.ptrace_scope=1 kernel.yama.ptrace_scope=1
kernel.randomize_va_space=2 kernel.randomize_va_space=2
# Network # Network
# Disable forwarding; override in inventory for routers/containers
net.ipv4.ip_forward=0 net.ipv4.ip_forward=0
net.ipv4.tcp_syncookies=1 net.ipv4.tcp_syncookies=1
net.ipv4.icmp_echo_ignore_broadcasts=1 net.ipv4.icmp_echo_ignore_broadcasts=1
@@ -24,6 +27,7 @@
net.ipv4.conf.default.send_redirects=0 net.ipv4.conf.default.send_redirects=0
net.ipv4.conf.default.accept_redirects=0 net.ipv4.conf.default.accept_redirects=0
net.ipv6.conf.all.accept_redirects=0 net.ipv6.conf.all.accept_redirects=0
# Disable IPv6; override in inventory if IPv6 is needed
net.ipv6.conf.all.disable_ipv6=1 net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.accept_redirects=0 net.ipv6.conf.default.accept_redirects=0
net.ipv6.conf.default.disable_ipv6=1 net.ipv6.conf.default.disable_ipv6=1

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,4 +1,10 @@
--- ---
# Post-reboot verification
cleanup_verify_boot: true
cleanup_boot_timeout: 300
cleanup_remove_on_failure: true
# Libvirt paths
cleanup_libvirt_image_dir: >- cleanup_libvirt_image_dir: >-
{{ {{
system_cfg.path system_cfg.path

View File

@@ -99,6 +99,7 @@
name: "{{ hostname }}" name: "{{ hostname }}"
state: running 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 - name: Wait for VM to boot up
delegate_to: "{{ inventory_hostname }}" delegate_to: "{{ inventory_hostname }}"
ansible.builtin.wait_for_connection: ansible.builtin.wait_for_connection:

View File

@@ -3,25 +3,32 @@
when: hypervisor_type == "proxmox" when: hypervisor_type == "proxmox"
delegate_to: localhost delegate_to: localhost
become: false become: false
module_defaults:
community.proxmox.proxmox_disk:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
community.proxmox.proxmox_kvm:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
node: "{{ hypervisor_cfg.host }}"
block: block:
- name: Cleanup Setup Disks - name: Cleanup Setup Disks
community.proxmox.proxmox_disk: community.proxmox.proxmox_disk:
api_host: "{{ hypervisor_cfg.url }}"
api_user: "{{ hypervisor_cfg.username }}"
api_password: "{{ hypervisor_cfg.password }}"
name: "{{ hostname }}" name: "{{ hostname }}"
vmid: "{{ system_cfg.id }}" vmid: "{{ system_cfg.id }}"
disk: "{{ item }}" disk: "{{ item }}"
state: absent state: absent
loop: loop: >-
- ide0 {{
- ide2 ['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 - name: Start the VM
community.proxmox.proxmox_kvm: 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 }}" vmid: "{{ system_cfg.id }}"
state: restarted state: restarted

View File

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

View File

@@ -3,36 +3,55 @@
when: hypervisor_type == "vmware" when: hypervisor_type == "vmware"
delegate_to: localhost delegate_to: localhost
become: false become: false
module_defaults:
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 }}"
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 }}"
no_log: true
block: block:
- name: Remove CD-ROM from VM in vCenter - name: Remove CD-ROM from VM in vCenter
community.vmware.vmware_guest: 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 }}" name: "{{ hostname }}"
cdrom: cdrom: >-
- controller_number: 0 {{
unit_number: 0 [
controller_type: sata {
type: iso 'controller_number': 0,
iso_path: "{{ boot_iso }}" 'unit_number': 0,
state: absent 'controller_type': 'sata',
- controller_number: 0 'type': 'iso',
unit_number: 1 'iso_path': boot_iso,
controller_type: sata 'state': 'absent'
type: iso }
iso_path: "{{ rhel_iso if rhel_iso is defined and rhel_iso | length > 0 else omit }}" ]
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 failed_when: false
- name: Start VM in vCenter - name: Start VM in vCenter
vmware.vmware.vm_powerstate: 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 }}" name: "{{ hostname }}"
state: powered-on state: powered-on

View File

@@ -3,6 +3,8 @@
when: hypervisor_type == "xen" when: hypervisor_type == "xen"
delegate_to: localhost delegate_to: localhost
become: false become: false
vars:
xen_installer_media_enabled: "{{ xen_installer_media_enabled | default(false) }}"
block: block:
- name: Ensure Xen disk definitions exist - name: Ensure Xen disk definitions exist
when: virtualization_xen_disks is not defined when: virtualization_xen_disks is not defined

View File

@@ -23,6 +23,22 @@
- /mnt/etc/motd.d/insights-client - /mnt/etc/motd.d/insights-client
failed_when: false 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 - name: Configure sudo banner
when: system_cfg.features.banner.sudo | bool when: system_cfg.features.banner.sudo | bool
block: block:

View File

@@ -21,19 +21,19 @@
- name: Check existing EFI boot entries - name: Check existing EFI boot entries
ansible.builtin.command: efibootmgr ansible.builtin.command: efibootmgr
register: _efi_entries register: configuration_efi_entries
changed_when: false changed_when: false
- name: Ensure EFI boot entry exists - name: Ensure EFI boot entry exists
when: ('* ' + _efi_vendor) not in _efi_entries.stdout when: ('* ' + _efi_vendor) not in configuration_efi_entries.stdout
ansible.builtin.command: >- ansible.builtin.command: >-
efibootmgr -c efibootmgr -c
-L '{{ _efi_vendor }}' -L '{{ _efi_vendor }}'
-d '{{ install_drive }}' -d '{{ install_drive }}'
-p 1 -p 1
-l '\EFI\{{ _efi_vendor }}\{{ _efi_loader }}' -l '\EFI\{{ _efi_vendor }}\{{ _efi_loader }}'
register: _efi_entry_result register: configuration_efi_entry_result
changed_when: _efi_entry_result.rc == 0 changed_when: configuration_efi_entry_result.rc == 0
- name: Ensure lvm2 for non btrfs filesystems - name: Ensure lvm2 for non btrfs filesystems
when: os == "archlinux" and system_cfg.filesystem != "btrfs" when: os == "archlinux" and system_cfg.filesystem != "btrfs"

View File

@@ -59,6 +59,14 @@
when: configuration_luks_auto_method == 'keyfile' when: configuration_luks_auto_method == 'keyfile'
ansible.builtin.include_tasks: encryption/keyfile.yml 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 - name: Build LUKS parameters
vars: vars:
luks_keyfile_in_use: "{{ configuration_luks_auto_method == 'keyfile' }}" luks_keyfile_in_use: "{{ configuration_luks_auto_method == 'keyfile' }}"
@@ -142,7 +150,7 @@
regexp: "^HOOKS=" regexp: "^HOOKS="
line: >- line: >-
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole
block sd-encrypt lvm2 filesystems fsck) block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck)
- name: Read mkinitcpio configuration - name: Read mkinitcpio configuration
when: os == 'archlinux' when: os == 'archlinux'
@@ -246,7 +254,7 @@
mode: "0644" mode: "0644"
content: "{{ configuration_kernel_cmdline_new }}\n" content: "{{ configuration_kernel_cmdline_new }}\n"
- name: Find BLS entries - name: Find BLS entries for encryption kernel cmdline
when: is_rhel | bool when: is_rhel | bool
ansible.builtin.find: ansible.builtin.find:
paths: /mnt/boot/loader/entries paths: /mnt/boot/loader/entries

View File

@@ -104,6 +104,13 @@
failed_when: false failed_when: false
no_log: true 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 - name: Fallback to manual LUKS unlock if keyfile enrollment failed
when: (configuration_luks_keyfile_unlock_test_after.rc | default(1)) != 0 when: (configuration_luks_keyfile_unlock_test_after.rc | default(1)) != 0
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

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

View File

@@ -9,7 +9,7 @@
set smartindent set smartindent
set mouse=a set mouse=a
insertafter: EOF insertafter: EOF
marker: "" marker: "# {mark} CUSTOM VIM CONFIG"
failed_when: false failed_when: false
- name: Add memory tuning parameters - name: Add memory tuning parameters
@@ -22,7 +22,7 @@
vm.dirty_background_ratio=1 vm.dirty_background_ratio=1
vm.dirty_ratio=10 vm.dirty_ratio=10
vm.page-cluster=10 vm.page-cluster=10
marker: "" marker: "# {mark} MEMORY TUNING"
mode: "0644" mode: "0644"
- name: Create zram config - name: Create zram config
@@ -45,28 +45,3 @@
src: custom.sh.j2 src: custom.sh.j2
dest: /mnt/etc/profile.d/custom.sh dest: /mnt/etc/profile.d/custom.sh
mode: "0644" 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)" regexp: "(xfs.*?)(attr2)"
replace: "\\1allocsize=64m" 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 - name: Replace ISO UUID entry with /dev/sr0 in fstab
when: os == "rhel" when:
- os == "rhel"
- system_cfg.features.rhel_repo.source == "iso"
vars: vars:
configuration_fstab_dvd_line: >- configuration_fstab_dvd_line: >-
{{ {{
@@ -39,7 +50,10 @@
state: present state: present
- name: Write image from RHEL ISO to the target machine - 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: ansible.builtin.command:
argv: argv:
- dd - dd
@@ -63,3 +77,4 @@
- { regexp: "^tmpfs\\s+/dev/shm\\s+", line: "tmpfs /dev/shm tmpfs defaults,nosuid,nodev,noexec 0 0" } - { regexp: "^tmpfs\\s+/dev/shm\\s+", line: "tmpfs /dev/shm tmpfs defaults,nosuid,nodev,noexec 0 0" }
loop_control: loop_control:
loop_var: fstab_entry loop_var: fstab_entry
label: "{{ fstab_entry.regexp }}"

View File

@@ -10,6 +10,8 @@
line: GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3" line: GRUB_CMDLINE_LINUX_DEFAULT="loglevel=3"
- regexp: ^GRUB_TIMEOUT= - regexp: ^GRUB_TIMEOUT=
line: GRUB_TIMEOUT=1 line: GRUB_TIMEOUT=1
loop_control:
label: "{{ item.line }}"
- name: Ensure grub defaults file exists for RHEL-based systems - name: Ensure grub defaults file exists for RHEL-based systems
when: is_rhel | bool when: is_rhel | bool
@@ -95,7 +97,7 @@
mode: "0644" mode: "0644"
content: "{{ configuration_kernel_cmdline_base }}\n" content: "{{ configuration_kernel_cmdline_base }}\n"
- name: Find BLS entries - name: Find BLS entries for GRUB configuration
ansible.builtin.find: ansible.builtin.find:
paths: /mnt/boot/loader/entries paths: /mnt/boot/loader/entries
patterns: "*.conf" patterns: "*.conf"

View File

@@ -21,6 +21,8 @@
line: "{{ item.line }}" line: "{{ item.line }}"
loop: loop:
- { regex: "{{ system_cfg.locale }} UTF-8", line: "{{ system_cfg.locale }} UTF-8" } - { regex: "{{ system_cfg.locale }} UTF-8", line: "{{ system_cfg.locale }} UTF-8" }
loop_control:
label: "{{ item.line }}"
- name: Generate locales - name: Generate locales
when: not is_rhel | bool when: not is_rhel | bool
@@ -45,7 +47,7 @@
- name: Set hostname - name: Set hostname
ansible.builtin.copy: ansible.builtin.copy:
content: "{{ configuration_hostname_fqdn }}" content: "{{ configuration_hostname_fqdn.split('.')[0] }}"
dest: /mnt/etc/hostname dest: /mnt/etc/hostname
mode: "0644" mode: "0644"

View File

@@ -1,19 +1,23 @@
--- ---
- name: Include configuration tasks - name: Include configuration tasks
ansible.builtin.include_tasks: "{{ configuration_task }}" when: configuration_task.when | default(true)
ansible.builtin.include_tasks: "{{ configuration_task.file }}"
loop: loop:
- banner.yml - file: banner.yml
- fstab.yml - file: fstab.yml
- locales.yml - file: locales.yml
- ssh.yml - file: ssh.yml
- services.yml - file: services.yml
- grub.yml - file: grub.yml
- encryption.yml - file: encryption.yml
- bootloader.yml when: "{{ system_cfg.luks.enabled | bool }}"
- extras.yml - file: bootloader.yml
- network.yml - file: extras.yml
- users.yml - file: network.yml
- sudo.yml - file: users.yml
- selinux.yml - file: sudo.yml
- file: selinux.yml
when: "{{ is_rhel | bool }}"
loop_control: loop_control:
loop_var: configuration_task loop_var: configuration_task
label: "{{ configuration_task.file }}"

View File

@@ -29,88 +29,9 @@
- configuration_detected_interfaces | length > 0 - configuration_detected_interfaces | length > 0
fail_msg: Failed to detect any network interfaces. fail_msg: Failed to detect any network interfaces.
- name: Configure NetworkManager profiles - name: Configure networking
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: 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: vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}" configuration_network_task_map:
block: alpine: network_alpine.yml
- name: Write Alpine network interfaces void: network_void.yml
ansible.builtin.copy: ansible.builtin.include_tasks: "{{ configuration_network_task_map[os] | default('network_nm.yml') }}"
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 %}

View File

@@ -0,0 +1,37 @@
---
- name: Write Alpine network interfaces
vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
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
vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
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 %}

View File

@@ -0,0 +1,26 @@
---
- name: Copy NetworkManager keyfile per interface
vars:
configuration_iface: "{{ item }}"
configuration_iface_name: >-
{{
item.name
if (item.name | default('') | string | length) > 0
else (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: 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,25 @@
---
- name: Write dhcpcd configuration
vars:
configuration_dns_list: "{{ system_cfg.network.dns.servers | default([]) }}"
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 %}
{% endif %}
{% endfor %}

View File

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

View File

@@ -1,22 +1,30 @@
--- ---
- name: Set root password - name: Set root password
vars: ansible.builtin.shell: >-
configuration_root_cmd: >- set -o pipefail &&
{{ chroot_command }} /usr/sbin/usermod --password echo 'root:{{ system_cfg.root.password | password_hash("sha512") }}' | {{ chroot_command }} chpasswd -e
'{{ system_cfg.root.password | password_hash('sha512') }}' root --shell /bin/bash args:
ansible.builtin.command: "{{ configuration_root_cmd }}" executable: /bin/bash
register: configuration_root_result register: configuration_root_result
changed_when: configuration_root_result.rc == 0 changed_when: configuration_root_result.rc == 0
no_log: true
- name: Set root shell
ansible.builtin.command: >-
{{ chroot_command }} /usr/sbin/usermod --shell {{ system_cfg.root.shell | default('/bin/bash') }} root
register: configuration_root_shell_result
changed_when: configuration_root_shell_result.rc == 0
- name: Create user accounts - name: Create user accounts
vars: vars:
configuration_user_group: >- configuration_user_group: >-
{{ "sudo" if is_debian | bool else "wheel" }} {{ "sudo" if is_debian | bool else "wheel" }}
# UID starts at 1000; safe for fresh installs only
configuration_useradd_cmd: >- configuration_useradd_cmd: >-
{{ chroot_command }} /usr/sbin/useradd --create-home --user-group {{ chroot_command }} /usr/sbin/useradd --create-home --user-group
--uid {{ 1000 + ansible_loop.index0 }} --uid {{ 1000 + ansible_loop.index0 }}
--groups {{ configuration_user_group }} {{ item.name }} --groups {{ configuration_user_group }} {{ item.name }}
--password {{ item.password | password_hash('sha512') }} --shell /bin/bash --password {{ item.password | password_hash('sha512') }} --shell {{ item.shell | default('/bin/bash') }}
ansible.builtin.command: "{{ configuration_useradd_cmd }}" ansible.builtin.command: "{{ configuration_useradd_cmd }}"
loop: "{{ system_cfg.users }}" loop: "{{ system_cfg.users }}"
loop_control: loop_control:
@@ -24,6 +32,7 @@
label: "{{ item.name }}" label: "{{ item.name }}"
register: configuration_user_result register: configuration_user_result
changed_when: configuration_user_result.rc == 0 changed_when: configuration_user_result.rc == 0
no_log: true
- name: Ensure .ssh directory exists - name: Ensure .ssh directory exists
when: item['keys'] | default([]) | length > 0 when: item['keys'] | default([]) | length > 0

View File

@@ -15,11 +15,11 @@ method=manual
method=auto method=auto
{% endif %} {% endif %}
{% if idx | int == 0 and dns_list %} {% if idx | int == 0 and dns_list %}
dns={{ dns_list | join(';') }} dns={{ dns_list | join(';') }};
ignore-auto-dns=true ignore-auto-dns=true
{% endif %} {% endif %}
{% if idx | int == 0 and search_list %} {% if idx | int == 0 and search_list %}
dns-search={{ search_list | join(';') }} dns-search={{ search_list | join(';') }};
{% endif %} {% endif %}
[ipv6] [ipv6]

View File

@@ -132,6 +132,8 @@
replace: "PermitEmptyPasswords yes" replace: "PermitEmptyPasswords yes"
- regexp: "^#?PermitRootLogin.*" - regexp: "^#?PermitRootLogin.*"
replace: "PermitRootLogin yes" replace: "PermitRootLogin yes"
loop_control:
label: "{{ item.replace }}"
- name: Reload SSH service to apply changes - name: Reload SSH service to apply changes
ansible.builtin.service: ansible.builtin.service:
@@ -175,6 +177,8 @@
- { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] } - { name: debootstrap, os: [debian, ubuntu, ubuntu-lts] }
- { name: debian-archive-keyring, os: [debian] } - { name: debian-archive-keyring, os: [debian] }
- { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] } - { name: ubuntu-keyring, os: [ubuntu, ubuntu-lts] }
loop_control:
label: "{{ item.name }}"
retries: 4 retries: 4
delay: 15 delay: 15
@@ -187,15 +191,29 @@
state: directory state: directory
mode: "0755" 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 - 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: ansible.builtin.set_fact:
environment_rhel_iso_device: >- environment_rhel_iso_device: >-
{{ {{
'/dev/sr2' _rom_devices[-1]
if hypervisor_type == 'libvirt' if _rom_devices | length > 1
else '/dev/sr1' else (_rom_devices[0] | default('/dev/sr1'))
}} }}
changed_when: false
- name: Mount RHEL ISO - name: Mount RHEL ISO
ansible.posix.mount: ansible.posix.mount:
@@ -205,6 +223,10 @@
opts: "ro,loop" opts: "ro,loop"
state: mounted 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: Relax RPM Sequoia signature policy for RHEL bootstrap - name: Relax RPM Sequoia signature policy for RHEL bootstrap
when: is_rhel | bool when: is_rhel | bool
ansible.builtin.copy: ansible.builtin.copy:

View File

@@ -1,4 +1,27 @@
--- ---
# 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_supported:
- almalinux
- alpine
- archlinux
- debian
- fedora
- opensuse
- rhel
- rocky
- ubuntu
- ubuntu-lts
- void
# User input. Normalized into hypervisor_cfg + hypervisor_type. # User input. Normalized into hypervisor_cfg + hypervisor_type.
hypervisor: hypervisor:
type: "none" type: "none"
@@ -83,6 +106,9 @@ system_defaults:
banner: banner:
motd: false motd: false
sudo: true sudo: true
rhel_repo:
source: "iso" # iso|satellite|none — how RHEL systems get packages post-install
url: "" # Satellite/custom repo URL when source=satellite
chroot: chroot:
tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn tool: "arch-chroot" # arch-chroot|chroot|systemd-nspawn

View File

@@ -0,0 +1,100 @@
---
- 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 }}"

View File

@@ -0,0 +1,143 @@
---
- 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 | 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 []
)
}}
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 }}"
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 ('archlinux' 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 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

View File

@@ -18,3 +18,4 @@
ansible.builtin.set_fact: ansible.builtin.set_fact:
hypervisor_cfg: "{{ merged }}" hypervisor_cfg: "{{ merged }}"
hypervisor_type: "{{ merged.type | string | lower }}" hypervisor_type: "{{ merged.type | string | lower }}"
no_log: true

View File

@@ -14,8 +14,8 @@
- name: Set OS family flags - name: Set OS family flags
ansible.builtin.set_fact: ansible.builtin.set_fact:
is_rhel: "{{ os in ['almalinux', 'fedora', 'rhel', 'rocky'] }}" is_rhel: "{{ os in os_family_rhel }}"
is_debian: "{{ os in ['debian', 'ubuntu', 'ubuntu-lts'] }}" is_debian: "{{ os in os_family_debian }}"
- name: Normalize OS version for keying - name: Normalize OS version for keying
when: when:
@@ -49,6 +49,7 @@
ansible_password: "{{ system_cfg.users[0].password }}" ansible_password: "{{ system_cfg.users[0].password }}"
ansible_become_password: "{{ system_cfg.users[0].password }}" ansible_become_password: "{{ system_cfg.users[0].password }}"
ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" ansible_ssh_extra_args: "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
no_log: true
- name: Set connection for VMware - name: Set connection for VMware
when: hypervisor_type == "vmware" when: hypervisor_type == "vmware"

View File

@@ -1,297 +1,9 @@
--- ---
- name: Ensure system input is a dictionary - name: Validate raw system input types
ansible.builtin.set_fact: ansible.builtin.include_tasks: _validate_input.yml
system: "{{ system | default({}) }}"
- name: Validate system input types - name: Normalize system configuration
ansible.builtin.assert: ansible.builtin.include_tasks: _normalize_system.yml
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
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)
}}
- 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: Normalize disk configuration
ansible.builtin.include_tasks: _normalize_disks.yml

View File

@@ -114,7 +114,7 @@
ansible.builtin.assert: ansible.builtin.assert:
that: that:
- os is defined - 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"] os not in ["debian", "fedora", "rocky", "almalinux", "rhel"]
or (os_version is defined and (os_version | string | length) > 0) or (os_version is defined and (os_version | string | length) > 0)
@@ -123,7 +123,7 @@
or ( or (
os == "debian" and (os_version | string) in ["10", "11", "12", "13", "unstable"] os == "debian" and (os_version | string) in ["10", "11", "12", "13", "unstable"]
) or ( ) or (
os == "fedora" and (os_version | string) in ["40", "41", "42", "43"] os == "fedora" and (os_version | int) >= 38 and (os_version | int) <= 45
) or ( ) or (
os in ["rocky", "almalinux"] os in ["rocky", "almalinux"]
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$") and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
@@ -131,7 +131,9 @@
os == "rhel" os == "rhel"
and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$") and (os_version | string) is match("^(8|9|10)(\\.\\d+)?$")
) or ( ) or (
os in ["alpine", "archlinux", "opensuse", "ubuntu", "ubuntu-lts", "void"] os in ["ubuntu", "ubuntu-lts"]
) or (
os in ["alpine", "archlinux", "opensuse", "void"]
) )
fail_msg: "Invalid os/version specified. Please check README.md for supported values." fail_msg: "Invalid os/version specified. Please check README.md for supported values."
quiet: true quiet: true
@@ -162,6 +164,7 @@
Missing required Proxmox inputs. Define hypervisor.(url,username,password,host,storage), Missing required Proxmox inputs. Define hypervisor.(url,username,password,host,storage),
system.id, and system.network.bridge (or system.network.interfaces[]). system.id, and system.network.bridge (or system.network.interfaces[]).
quiet: true quiet: true
no_log: true
- name: Validate VMware hypervisor inputs - name: Validate VMware hypervisor inputs
when: when:
@@ -182,6 +185,7 @@
Missing required VMware inputs. Define hypervisor.(url,username,password,datacenter,cluster,storage) Missing required VMware inputs. Define hypervisor.(url,username,password,datacenter,cluster,storage)
and system.network.bridge (or system.network.interfaces[]). and system.network.bridge (or system.network.interfaces[]).
quiet: true quiet: true
no_log: true
- name: Validate Xen hypervisor inputs - name: Validate Xen hypervisor inputs
when: when:
@@ -195,6 +199,18 @@
fail_msg: "Missing required Xen inputs. Define system.network.bridge (or system.network.interfaces[])." fail_msg: "Missing required Xen inputs. Define system.network.bridge (or system.network.interfaces[])."
quiet: true quiet: true
- name: Validate libvirt hypervisor inputs
when:
- system_cfg.type == "virtual"
- hypervisor_type == "libvirt"
ansible.builtin.assert:
that:
- >-
(system_cfg.network.bridge | default('') | string | length > 0)
or (system_cfg.network.interfaces | default([]) | length > 0)
fail_msg: "Missing required libvirt inputs. Define system.network.bridge (or system.network.interfaces[])."
quiet: true
- name: Validate virtual installer ISO requirement - name: Validate virtual installer ISO requirement
ansible.builtin.assert: ansible.builtin.assert:
that: that:
@@ -245,6 +261,28 @@
fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size." fail_msg: "Invalid system sizing. Check system.cpus, system.memory, and system.disks[0].size."
quiet: true quiet: true
- name: Validate at least one user is defined
ansible.builtin.assert:
that:
- system_cfg.users | default([]) | length > 0
- system_cfg.users[0].name is defined and (system_cfg.users[0].name | string | length) > 0
- system_cfg.users[0].password is defined and (system_cfg.users[0].password | string | length) > 0
fail_msg: "At least one user with a name and password must be defined in system.users[]."
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 - name: Validate all virtual disks have a positive size
when: system_cfg.type == "virtual" when: system_cfg.type == "virtual"
ansible.builtin.assert: ansible.builtin.assert:

View File

@@ -6,20 +6,40 @@ partitioning_efi_size_mib: 512
partitioning_efi_start_mib: 1 partitioning_efi_start_mib: 1
partitioning_efi_end_mib: "{{ (partitioning_efi_start_mib | int) + (partitioning_efi_size_mib | int) }}" partitioning_efi_end_mib: "{{ (partitioning_efi_start_mib | int) + (partitioning_efi_size_mib | int) }}"
partitioning_boot_size_mib: 1024 partitioning_boot_size_mib: 1024
partitioning_vg_name: sys
partitioning_use_full_disk: true 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: >- partitioning_separate_boot: >-
{{ {{
( (
(system_cfg.luks.enabled | bool) (system_cfg.luks.enabled | bool)
or (system_cfg.filesystem != 'btrfs') or (system_cfg.filesystem != 'btrfs')
) )
and (os not in ['archlinux']) and ((os | default('')) not in ['archlinux'])
}} }}
partitioning_boot_fs_fstype: >- partitioning_boot_fs_fstype: >-
{{ {{
system_cfg.filesystem system_cfg.filesystem
if system_cfg.filesystem != 'btrfs' 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: >- partitioning_boot_fs_partition_suffix: >-
{{ {{
@@ -37,7 +57,7 @@ partitioning_efi_mountpoint: >-
if (partitioning_separate_boot | bool) if (partitioning_separate_boot | bool)
else ( else (
'/boot/efi' '/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' else '/boot'
) )
}} }}

View File

@@ -54,13 +54,17 @@
- { subvol: pkg } - { subvol: pkg }
- { subvol: var_log } - { subvol: var_log }
- { subvol: var_log_audit } - { subvol: var_log_audit }
loop_control:
label: "{{ item.subvol }}"
register: partitioning_btrfs_subvol_result register: partitioning_btrfs_subvol_result
- name: Set quotas for subvolumes - name: Set quotas for subvolumes
when: system_cfg.features.cis.enabled when: system_cfg.features.cis.enabled
ansible.builtin.command: btrfs qgroup limit {{ item.quota }} /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }} ansible.builtin.command: btrfs qgroup limit {{ item.quota }} /mnt/{{ '@' if item.subvol == 'root' else '@' + item.subvol }}
loop: loop:
- { subvol: home, quota: 2G } - { subvol: home, quota: "{{ partitioning_btrfs_home_quota }}" }
loop_control:
label: "{{ item.subvol }}"
register: partitioning_btrfs_qgroup_result register: partitioning_btrfs_qgroup_result
changed_when: false changed_when: false

View File

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

View File

@@ -7,7 +7,6 @@
| selectattr('mount.path') | selectattr('mount.path')
| list | list
}} }}
changed_when: false
- name: Validate additional disks do not target install_drive - name: Validate additional disks do not target install_drive
when: partitioning_extra_disks | length > 0 when: partitioning_extra_disks | length > 0
@@ -59,7 +58,16 @@
dev: "{{ item.partition }}" dev: "{{ item.partition }}"
fstype: "{{ item.mount.fstype }}" fstype: "{{ item.mount.fstype }}"
opts: "{{ _all_opts }}" opts: "{{ _all_opts }}"
force: true loop: "{{ partitioning_extra_disks }}"
loop_control:
label: "{{ item.partition }}"
- name: Collect extra disk UUIDs
when: partitioning_extra_disks | length > 0
ansible.builtin.command: "blkid -s UUID -o value {{ item.partition }}"
register: partitioning_extra_disk_uuids
changed_when: false
failed_when: partitioning_extra_disk_uuids.rc != 0 or (partitioning_extra_disk_uuids.stdout | trim | length) == 0
loop: "{{ partitioning_extra_disks }}" loop: "{{ partitioning_extra_disks }}"
loop_control: loop_control:
label: "{{ item.partition }}" label: "{{ item.partition }}"
@@ -79,11 +87,11 @@
- name: Mount additional disks for fstab generation - name: Mount additional disks for fstab generation
when: partitioning_extra_disks | length > 0 when: partitioning_extra_disks | length > 0
ansible.posix.mount: ansible.posix.mount:
path: "/mnt{{ item.mount.path }}" path: "/mnt{{ item.0.mount.path }}"
src: "{{ item.partition }}" src: "UUID={{ item.1.stdout }}"
fstype: "{{ item.mount.fstype }}" fstype: "{{ item.0.mount.fstype }}"
opts: "{{ item.mount.opts | default('defaults') }}" opts: "{{ item.0.mount.opts | default('defaults') }}"
state: mounted state: mounted
loop: "{{ partitioning_extra_disks }}" loop: "{{ partitioning_extra_disks | zip(partitioning_extra_disk_uuids.results) | list }}"
loop_control: loop_control:
label: "{{ item.mount.path }}" label: "{{ item.0.mount.path }}"

View File

@@ -3,7 +3,7 @@
when: when:
- system_cfg.features.swap.enabled | bool - system_cfg.features.swap.enabled | bool
- partitioning_vm_memory is not defined or (partitioning_vm_memory | float) <= 0 - partitioning_vm_memory is not defined or (partitioning_vm_memory | float) <= 0
- system_cfg is not defined or (system_cfg.memory | default(0) | float) <= 0 - (system_cfg.memory | default(0) | float) <= 0
block: block:
- name: Read system memory - name: Read system memory
ansible.builtin.command: awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo ansible.builtin.command: awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo
@@ -65,7 +65,7 @@
- name: Remove LVM volume group - name: Remove LVM volume group
community.general.lvg: community.general.lvg:
vg: sys vg: "{{ partitioning_vg_name }}"
state: absent state: absent
force: true force: true
failed_when: false failed_when: false
@@ -93,10 +93,10 @@
failed_when: false failed_when: false
- name: Wipe filesystem signatures - name: Wipe filesystem signatures
ansible.builtin.command: >- ansible.builtin.shell: >-
find /dev -wholename "{{ install_drive }}*" -exec wipefs --force --all {} \; find /dev -wholename "{{ install_drive }}*" -exec wipefs --force --all {} \;
register: partitioning_wipefs_result register: partitioning_wipefs_result
changed_when: false changed_when: partitioning_wipefs_result.rc == 0
failed_when: false failed_when: false
- name: Refresh kernel partition table - name: Refresh kernel partition table
@@ -122,6 +122,8 @@
flags: "{{ item.flags | default(omit) }}" flags: "{{ item.flags | default(omit) }}"
state: present state: present
loop: "{{ partitioning_layout }}" loop: "{{ partitioning_layout }}"
loop_control:
label: "{{ item.name }}"
rescue: rescue:
- name: Refresh kernel partition table after failure - name: Refresh kernel partition table after failure
ansible.builtin.command: "{{ item }}" ansible.builtin.command: "{{ item }}"
@@ -144,6 +146,8 @@
flags: "{{ item.flags | default(omit) }}" flags: "{{ item.flags | default(omit) }}"
state: present state: present
loop: "{{ partitioning_layout }}" loop: "{{ partitioning_layout }}"
loop_control:
label: "{{ item.name }}"
- name: Settle partition table - name: Settle partition table
ansible.builtin.command: "{{ item }}" ansible.builtin.command: "{{ item }}"
@@ -251,7 +255,7 @@
block: block:
- name: Create LVM volume group - name: Create LVM volume group
community.general.lvg: community.general.lvg:
vg: sys vg: "{{ partitioning_vg_name }}"
pvs: "{{ partitioning_root_device }}" pvs: "{{ partitioning_root_device }}"
- name: Create LVM logical volumes - name: Create LVM logical volumes
@@ -272,10 +276,11 @@
partitioning_lvm_swap_target_gb: >- partitioning_lvm_swap_target_gb: >-
{{ {{
( (
[ ((partitioning_memory_mb | float / 1024) >= 16.0)
(partitioning_memory_mb | float / 1024), | ternary(
4 (partitioning_memory_mb | float / 2048),
] | max | float [(partitioning_memory_mb | float / 1024), 4] | max | float
)
) )
if system_cfg.features.swap.enabled | bool if system_cfg.features.swap.enabled | bool
else 0 else 0
@@ -285,7 +290,7 @@
( (
4 4
+ [ + [
(partitioning_disk_size_gb | float) - 20, (partitioning_disk_size_gb | float) - (partitioning_disk_overhead_gb | float),
0 0
] | max ] | max
) )
@@ -310,7 +315,7 @@
( (
(partitioning_disk_size_gb | float) (partitioning_disk_size_gb | float)
- (partitioning_reserved_gb | float) - (partitioning_reserved_gb | float)
- (system_cfg.features.cis.enabled | ternary(7.5, 0)) - (system_cfg.features.cis.enabled | ternary(partitioning_cis_reserved_gb | float, 0))
- partitioning_lvm_extent_reserve_gb - partitioning_lvm_extent_reserve_gb
- 4 - 4
), ),
@@ -325,14 +330,22 @@
( (
(partitioning_disk_size_gb | float) (partitioning_disk_size_gb | float)
- (partitioning_reserved_gb | float) - (partitioning_reserved_gb | float)
- (system_cfg.features.cis.enabled | ternary(7.5, 0)) - (system_cfg.features.cis.enabled | ternary(partitioning_cis_reserved_gb | float, 0))
- partitioning_lvm_extent_reserve_gb - partitioning_lvm_extent_reserve_gb
- partitioning_lvm_swap_target_limited_gb - partitioning_lvm_swap_target_limited_gb
) | float ) | 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_gb: >-
{{ {{
([([(((partitioning_disk_size_gb | float) - 20) * 0.1), 2] | max), 20] | min) [
[(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_root_default_gb: >-
{{ {{
@@ -374,7 +387,10 @@
- (partitioning_lvm_swap_gb | float) - (partitioning_lvm_swap_gb | float)
- partitioning_lvm_extent_reserve_gb - partitioning_lvm_extent_reserve_gb
- ( - (
(partitioning_lvm_home_gb | float) + 5.5 (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 if system_cfg.features.cis.enabled
else 0 else 0
) )
@@ -389,7 +405,7 @@
else partitioning_lvm_root_default_gb else partitioning_lvm_root_default_gb
}} }}
community.general.lvol: community.general.lvol:
vg: sys vg: "{{ partitioning_vg_name }}"
lv: "{{ item.lv }}" lv: "{{ item.lv }}"
size: "{{ item.size }}" size: "{{ item.size }}"
state: present state: present
@@ -400,9 +416,11 @@
size: "{{ partitioning_lvm_swap_gb | string + 'G' }}" size: "{{ partitioning_lvm_swap_gb | string + 'G' }}"
- lv: home - lv: home
size: "{{ partitioning_lvm_home_gb | string + 'G' }}" size: "{{ partitioning_lvm_home_gb | string + 'G' }}"
- { lv: var, size: "2G" } - { lv: var, size: "{{ partitioning_lvm_var_gb }}G" }
- { lv: var_log, size: "2G" } - { lv: var_log, size: "{{ partitioning_lvm_var_log_gb }}G" }
- { lv: var_log_audit, size: "1.5G" } - { lv: var_log_audit, size: "{{ partitioning_lvm_var_log_audit_gb }}G" }
loop_control:
label: "{{ item.lv }}"
- name: Create filesystems - name: Create filesystems
block: block:
@@ -438,7 +456,7 @@
- system_cfg.features.swap.enabled | bool - system_cfg.features.swap.enabled | bool
community.general.filesystem: community.general.filesystem:
fstype: swap fstype: swap
dev: /dev/sys/swap dev: /dev/{{ partitioning_vg_name }}/swap
- name: Create filesystem - name: Create filesystem
ansible.builtin.include_tasks: "{{ system_cfg.filesystem }}.yml" ansible.builtin.include_tasks: "{{ system_cfg.filesystem }}.yml"
@@ -447,6 +465,7 @@
ansible.builtin.command: blkid -s UUID -o value '{{ install_drive }}{{ partitioning_boot_partition_suffix }}' ansible.builtin.command: blkid -s UUID -o value '{{ install_drive }}{{ partitioning_boot_partition_suffix }}'
register: partitioning_boot_uuid register: partitioning_boot_uuid
changed_when: false changed_when: false
failed_when: partitioning_boot_uuid.rc != 0 or (partitioning_boot_uuid.stdout | trim | length) == 0
- name: Get UUID for /boot filesystem - name: Get UUID for /boot filesystem
when: partitioning_separate_boot | bool when: partitioning_separate_boot | bool
@@ -454,57 +473,65 @@
blkid -s UUID -o value '{{ install_drive }}{{ partitioning_boot_fs_partition_suffix }}' blkid -s UUID -o value '{{ install_drive }}{{ partitioning_boot_fs_partition_suffix }}'
register: partitioning_boot_fs_uuid register: partitioning_boot_fs_uuid
changed_when: false 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 - name: Get UUID for main filesystem
ansible.builtin.command: blkid -s UUID -o value '{{ partitioning_root_device }}' ansible.builtin.command: blkid -s UUID -o value '{{ partitioning_root_device }}'
register: partitioning_main_uuid register: partitioning_main_uuid
changed_when: false 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 - name: Get UUID for LVM root filesystem
when: system_cfg.filesystem != 'btrfs' when: system_cfg.filesystem != 'btrfs'
ansible.builtin.command: blkid -s UUID -o value /dev/sys/root ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/root
register: partitioning_uuid_root_result register: partitioning_uuid_root_result
changed_when: false 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 - name: Get UUID for LVM swap filesystem
when: when:
- system_cfg.filesystem != 'btrfs' - system_cfg.filesystem != 'btrfs'
- system_cfg.features.swap.enabled | bool - system_cfg.features.swap.enabled | bool
ansible.builtin.command: blkid -s UUID -o value /dev/sys/swap ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/swap
register: partitioning_uuid_swap_result register: partitioning_uuid_swap_result
changed_when: false 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 - name: Get UUID for LVM home filesystem
when: when:
- system_cfg.filesystem != 'btrfs' - system_cfg.filesystem != 'btrfs'
- system_cfg.features.cis.enabled - system_cfg.features.cis.enabled
ansible.builtin.command: blkid -s UUID -o value /dev/sys/home ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/home
register: partitioning_uuid_home_result register: partitioning_uuid_home_result
changed_when: false 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 - name: Get UUID for LVM var filesystem
when: when:
- system_cfg.filesystem != 'btrfs' - system_cfg.filesystem != 'btrfs'
- system_cfg.features.cis.enabled - system_cfg.features.cis.enabled
ansible.builtin.command: blkid -s UUID -o value /dev/sys/var ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var
register: partitioning_uuid_var_result register: partitioning_uuid_var_result
changed_when: false 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 - name: Get UUID for LVM var_log filesystem
when: when:
- system_cfg.filesystem != 'btrfs' - system_cfg.filesystem != 'btrfs'
- system_cfg.features.cis.enabled - system_cfg.features.cis.enabled
ansible.builtin.command: blkid -s UUID -o value /dev/sys/var_log ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var_log
register: partitioning_uuid_var_log_result register: partitioning_uuid_var_log_result
changed_when: false 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 - name: Get UUID for LVM var_log_audit filesystem
when: when:
- system_cfg.filesystem != 'btrfs' - system_cfg.filesystem != 'btrfs'
- system_cfg.features.cis.enabled - system_cfg.features.cis.enabled
ansible.builtin.command: blkid -s UUID -o value /dev/sys/var_log_audit ansible.builtin.command: blkid -s UUID -o value /dev/{{ partitioning_vg_name }}/var_log_audit
register: partitioning_uuid_var_log_audit_result register: partitioning_uuid_var_log_audit_result
changed_when: false 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 - name: Assign UUIDs to Variables
when: system_cfg.filesystem != 'btrfs' when: system_cfg.filesystem != 'btrfs'
@@ -514,31 +541,31 @@
{{ {{
partitioning_uuid_swap_result.stdout_lines | default([]) partitioning_uuid_swap_result.stdout_lines | default([])
if system_cfg.features.swap.enabled | bool if system_cfg.features.swap.enabled | bool
else '' else []
}} }}
partitioning_uuid_home: >- partitioning_uuid_home: >-
{{ {{
partitioning_uuid_home_result.stdout_lines | default([]) partitioning_uuid_home_result.stdout_lines | default([])
if system_cfg.features.cis.enabled if system_cfg.features.cis.enabled
else '' else []
}} }}
partitioning_uuid_var: >- partitioning_uuid_var: >-
{{ {{
partitioning_uuid_var_result.stdout_lines | default([]) partitioning_uuid_var_result.stdout_lines | default([])
if system_cfg.features.cis.enabled if system_cfg.features.cis.enabled
else '' else []
}} }}
partitioning_uuid_var_log: >- partitioning_uuid_var_log: >-
{{ {{
partitioning_uuid_var_log_result.stdout_lines | default([]) partitioning_uuid_var_log_result.stdout_lines | default([])
if system_cfg.features.cis.enabled if system_cfg.features.cis.enabled
else '' else []
}} }}
partitioning_uuid_var_log_audit: >- partitioning_uuid_var_log_audit: >-
{{ {{
partitioning_uuid_var_log_audit_result.stdout_lines | default([]) partitioning_uuid_var_log_audit_result.stdout_lines | default([])
if system_cfg.features.cis.enabled if system_cfg.features.cis.enabled
else '' else []
}} }}
- name: Mount filesystems - name: Mount filesystems
@@ -562,6 +589,7 @@
opts: "{{ item.opts }}" opts: "{{ item.opts }}"
state: mounted state: mounted
loop: loop:
# ssd: no-op on kernels 5.15+ (btrfs auto-detects); kept for older kernel compat
- path: "" - path: ""
uuid: "{{ partitioning_uuid_root[0] | default(omit) }}" uuid: "{{ partitioning_uuid_root[0] | default(omit) }}"
opts: >- opts: >-
@@ -636,6 +664,8 @@
'ssd', 'space_cache=v2', 'discard=async', 'subvol=@var_log_audit' 'ssd', 'space_cache=v2', 'discard=async', 'subvol=@var_log_audit'
] | reject('equalto', '') | join(',') ] | reject('equalto', '') | join(',')
}} }}
loop_control:
label: "{{ item.path }}"
- name: Mount /boot filesystem - name: Mount /boot filesystem
when: partitioning_separate_boot | bool when: partitioning_separate_boot | bool

View File

@@ -2,7 +2,7 @@
- name: Create and format XFS logical volumes - name: Create and format XFS 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 or item.lv not in ['home', 'var', 'var_log', 'var_log_audit']
community.general.filesystem: community.general.filesystem:
dev: /dev/sys/{{ item.lv }} dev: /dev/{{ partitioning_vg_name }}/{{ item.lv }}
fstype: xfs fstype: xfs
opts: "{{ '-m bigtime=0 -i nrext64=0,exchange=0 -n parent=0' if is_rhel | bool else omit }}" opts: "{{ '-m bigtime=0 -i nrext64=0,exchange=0 -n parent=0' if is_rhel | bool else omit }}"
force: true force: true
@@ -12,3 +12,5 @@
- { lv: var } - { lv: var }
- { lv: var_log } - { lv: var_log }
- { lv: var_log_audit } - { lv: var_log_audit }
loop_control:
label: "{{ item.lv }}"

View File

@@ -1,4 +1,14 @@
--- ---
- name: Physical install safety confirmation
when: system_cfg.type == "physical"
ansible.builtin.assert:
that:
- physical_install_confirmed | default(false) | bool
fail_msg: >-
DANGER: Physical install will WIPE {{ install_drive }} on {{ inventory_hostname }}.
Set physical_install_confirmed=true in inventory to proceed.
quiet: true
- name: VM existence protection check - name: VM existence protection check
when: system_cfg.type == "virtual" when: system_cfg.type == "virtual"
block: block:
@@ -23,68 +33,75 @@
Please choose a different hostname or remove the existing VM manually before proceeding. Please choose a different hostname or remove the existing VM manually before proceeding.
quiet: true quiet: true
- name: Check if VM already exists on Proxmox - name: Check VM existence on Proxmox
when: hypervisor_type == "proxmox" when: hypervisor_type == "proxmox"
delegate_to: localhost delegate_to: localhost
become: false become: false
community.proxmox.proxmox_vm_info: module_defaults:
api_host: "{{ hypervisor_cfg.url }}" community.proxmox.proxmox_vm_info:
api_user: "{{ hypervisor_cfg.username }}" api_host: "{{ hypervisor_cfg.url }}"
api_password: "{{ hypervisor_cfg.password }}" api_user: "{{ hypervisor_cfg.username }}"
node: "{{ hypervisor_cfg.host }}" api_password: "{{ hypervisor_cfg.password }}"
vmid: "{{ system_cfg.id }}" block:
name: "{{ hostname }}" - name: Query Proxmox for existing VM
type: qemu community.proxmox.proxmox_vm_info:
register: system_check_proxmox_check_result node: "{{ hypervisor_cfg.host }}"
changed_when: false vmid: "{{ system_cfg.id }}"
name: "{{ hostname }}"
type: qemu
register: system_check_proxmox_check_result
changed_when: false
no_log: true
- name: Abort if VM already exists on Proxmox - name: Abort if VM already exists on Proxmox
when: hypervisor_type == "proxmox" ansible.builtin.assert:
ansible.builtin.assert: that:
that: - system_check_proxmox_check_result.proxmox_vms | default([]) | length == 0
- system_check_proxmox_check_result.proxmox_vms | default([]) | length == 0 fail_msg: |
fail_msg: | VM {{ hostname }} (ID: {{ system_cfg.id }}) already exists on Proxmox hypervisor.
VM {{ hostname }} (ID: {{ system_cfg.id }}) already exists on Proxmox hypervisor. To avoid data loss, the playbook will not overwrite or delete existing VMs.
To avoid data loss, the playbook will not overwrite or delete existing VMs. Please choose a different hostname or VM ID, or remove the existing VM manually before proceeding.
Please choose a different hostname or VM ID, or remove the existing VM manually before proceeding. quiet: true
quiet: true
- name: Check if VM already exists in vCenter - name: Check VM existence in vCenter
when: hypervisor_type == "vmware" when: hypervisor_type == "vmware"
delegate_to: localhost delegate_to: localhost
community.vmware.vmware_guest_info: module_defaults:
hostname: "{{ hypervisor_cfg.url }}" community.vmware.vmware_guest_info:
username: "{{ hypervisor_cfg.username }}" hostname: "{{ hypervisor_cfg.url }}"
password: "{{ hypervisor_cfg.password }}" username: "{{ hypervisor_cfg.username }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}" password: "{{ hypervisor_cfg.password }}"
datacenter: "{{ hypervisor_cfg.datacenter }}" validate_certs: "{{ hypervisor_cfg.certs | bool }}"
name: "{{ hostname }}" block:
folder: "{{ system_cfg.path if system_cfg.path | length > 0 else omit }}" - name: Query vCenter for existing VM
register: system_check_vmware_check_result community.vmware.vmware_guest_info:
failed_when: false datacenter: "{{ hypervisor_cfg.datacenter }}"
changed_when: false name: "{{ hostname }}"
folder: "{{ system_cfg.path if system_cfg.path | length > 0 else omit }}"
register: system_check_vmware_check_result
failed_when: false
changed_when: false
no_log: true
- name: Fail if vCenter lookup failed unexpectedly - name: Fail if vCenter lookup failed unexpectedly
when: hypervisor_type == "vmware" ansible.builtin.assert:
ansible.builtin.assert: that:
that: - not system_check_vmware_check_result.failed
- not system_check_vmware_check_result.failed or (system_check_vmware_check_result.msg is search('non-existing VM'))
or (system_check_vmware_check_result.msg is search('non-existing VM')) fail_msg: |
fail_msg: | Unable to verify VM existence in vCenter.
Unable to verify VM existence in vCenter. {{ system_check_vmware_check_result.msg | default('Unknown error') }}
{{ system_check_vmware_check_result.msg | default('Unknown error') }} quiet: true
quiet: true
- name: Abort if VM already exists in vCenter - name: Abort if VM already exists in vCenter
when: hypervisor_type == "vmware" ansible.builtin.assert:
ansible.builtin.assert: that:
that: - system_check_vmware_check_result.instance is not defined
- system_check_vmware_check_result.instance is not defined fail_msg: |
fail_msg: | VM {{ hostname }} already exists in vCenter.
VM {{ hostname }} already exists in vCenter. To avoid data loss, the playbook will not overwrite or delete existing VMs.
To avoid data loss, the playbook will not overwrite or delete existing VMs. Please choose a different hostname or remove the existing VM manually before proceeding.
Please choose a different hostname or remove the existing VM manually before proceeding. quiet: true
quiet: true
- name: Check if VM already exists on Xen - name: Check if VM already exists on Xen
when: hypervisor_type == "xen" when: hypervisor_type == "xen"

View File

@@ -11,6 +11,10 @@ virtualization_libvirt_cloudinit_path: >-
{{ [virtualization_libvirt_image_dir, hostname ~ '-cloudinit.iso'] | ansible.builtin.path_join }} {{ [virtualization_libvirt_image_dir, hostname ~ '-cloudinit.iso'] | ansible.builtin.path_join }}
virtualization_xen_disk_path: /var/lib/xen/images virtualization_xen_disk_path: /var/lib/xen/images
virtualization_libvirt_machine_type: q35
virtualization_libvirt_ovmf_code: /usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd
virtualization_libvirt_ovmf_vars: /usr/share/edk2/x64/OVMF_VARS.4m.fd
virtualization_tpm2_enabled: >- virtualization_tpm2_enabled: >-
{{ {{
(system_cfg.luks.enabled | bool) (system_cfg.luks.enabled | bool)

View File

@@ -23,7 +23,6 @@
loop_control: loop_control:
label: "{{ item | to_json }}" label: "{{ item | to_json }}"
extended: true extended: true
changed_when: false
- name: Create VM disks - name: Create VM disks
delegate_to: localhost delegate_to: localhost
@@ -61,6 +60,7 @@
- "/tmp/cloud-network-config-{{ hostname }}.yml" - "/tmp/cloud-network-config-{{ hostname }}.yml"
creates: "{{ virtualization_libvirt_cloudinit_path }}" creates: "{{ virtualization_libvirt_cloudinit_path }}"
# uri defaults to qemu:///system (local libvirtd)
- name: Create VM using libvirt - name: Create VM using libvirt
delegate_to: localhost delegate_to: localhost
community.libvirt.virt: community.libvirt.virt:

View File

@@ -1,93 +1,94 @@
--- ---
- name: Deploy VM on Proxmox - name: Deploy VM on Proxmox
delegate_to: localhost delegate_to: localhost
vars: module_defaults:
virtualization_proxmox_scsi: >- community.proxmox.proxmox_kvm:
{%- set out = {} -%} api_host: "{{ hypervisor_cfg.url }}"
{%- for disk in system_cfg.disks -%} api_user: "{{ hypervisor_cfg.username }}"
{%- set _ = out.update({ 'scsi' ~ loop.index0: hypervisor_cfg.storage ~ ':' ~ (disk.size | int) }) -%} api_password: "{{ hypervisor_cfg.password }}"
{%- endfor -%} node: "{{ hypervisor_cfg.host }}"
{{ out }} block:
virtualization_proxmox_net: >- - name: Create VM on Proxmox
{%- set out = {} -%} vars:
{%- for iface in system_cfg.network.interfaces -%} virtualization_proxmox_scsi: >-
{%- set val = 'virtio,bridge=' ~ iface.bridge -%} {%- set out = {} -%}
{%- if iface.vlan | default('') | string | length > 0 -%} {%- for disk in system_cfg.disks -%}
{%- set val = val ~ ',tag=' ~ iface.vlan -%} {%- set _ = out.update({ 'scsi' ~ loop.index0: hypervisor_cfg.storage ~ ':' ~ (disk.size | int) }) -%}
{%- endif -%} {%- endfor -%}
{%- set _ = out.update({ 'net' ~ loop.index0: val }) -%} {{ out }}
{%- endfor -%} virtualization_proxmox_net: >-
{{ out }} {%- set out = {} -%}
virtualization_proxmox_ipconfig: >- {%- for iface in system_cfg.network.interfaces -%}
{%- set out = {} -%} {%- set val = 'virtio,bridge=' ~ iface.bridge -%}
{%- for iface in system_cfg.network.interfaces -%} {%- if iface.vlan | default('') | string | length > 0 -%}
{%- if iface.ip | default('') | string | length > 0 -%} {%- set val = val ~ ',tag=' ~ iface.vlan -%}
{%- set val = 'ip=' ~ iface.ip ~ '/' ~ iface.prefix {%- endif -%}
~ ((',gw=' ~ iface.gateway) if (iface.gateway | default('') | length > 0) else '') -%} {%- set _ = out.update({ 'net' ~ loop.index0: val }) -%}
{%- else -%} {%- endfor -%}
{%- set val = 'ip=dhcp' -%} {{ out }}
{%- endif -%} virtualization_proxmox_ipconfig: >-
{%- set _ = out.update({ 'ipconfig' ~ loop.index0: val }) -%} {%- set out = {} -%}
{%- endfor -%} {%- for iface in system_cfg.network.interfaces -%}
{{ out }} {%- if iface.ip | default('') | string | length > 0 -%}
community.proxmox.proxmox_kvm: {%- set val = 'ip=' ~ iface.ip ~ '/' ~ iface.prefix
api_host: "{{ hypervisor_cfg.url }}" ~ ((',gw=' ~ iface.gateway) if (iface.gateway | default('') | length > 0) else '') -%}
api_user: "{{ hypervisor_cfg.username }}" {%- else -%}
api_password: "{{ hypervisor_cfg.password }}" {%- set val = 'ip=dhcp' -%}
ciuser: "{{ system_cfg.users[0].name }}" {%- endif -%}
cipassword: "{{ system_cfg.users[0].password }}" {%- set _ = out.update({ 'ipconfig' ~ loop.index0: val }) -%}
ciupgrade: false {%- endfor -%}
node: "{{ hypervisor_cfg.host }}" {{ out }}
vmid: "{{ system_cfg.id }}" community.proxmox.proxmox_kvm:
name: "{{ hostname }}" ciuser: "{{ system_cfg.users[0].name }}"
cpu: host cipassword: "{{ system_cfg.users[0].password }}"
cores: "{{ system_cfg.cpus }}" ciupgrade: false
memory: "{{ system_cfg.memory }}" vmid: "{{ system_cfg.id }}"
balloon: "{{ system_cfg.balloon if system_cfg.balloon is defined and system_cfg.balloon | int > 0 else omit }}" name: "{{ hostname }}"
numa_enabled: true cpu: host
hotplug: network,disk cores: "{{ system_cfg.cpus }}"
update: "{{ virtualization_tpm2_enabled | bool }}" memory: "{{ system_cfg.memory }}"
update_unsafe: "{{ virtualization_tpm2_enabled | bool }}" balloon: "{{ system_cfg.balloon if system_cfg.balloon is defined and system_cfg.balloon | int > 0 else omit }}"
bios: ovmf numa_enabled: true
machine: "{{ 'q35' if virtualization_tpm2_enabled | bool else omit }}" hotplug: network,disk
boot: ac update: "{{ virtualization_tpm2_enabled | bool }}"
scsihw: virtio-scsi-single update_unsafe: "{{ virtualization_tpm2_enabled | bool }}"
scsi: "{{ virtualization_proxmox_scsi }}" bios: ovmf
efidisk0: machine: "{{ 'q35' if virtualization_tpm2_enabled | bool else omit }}"
efitype: 4m boot: ac
format: raw scsihw: virtio-scsi-single
pre_enrolled_keys: false scsi: "{{ virtualization_proxmox_scsi }}"
storage: "{{ hypervisor_cfg.storage }}" efidisk0:
tpmstate0: >- efitype: 4m
{{ format: raw
{'storage': hypervisor_cfg.storage, 'version': '2.0'} pre_enrolled_keys: false
if virtualization_tpm2_enabled | bool storage: "{{ hypervisor_cfg.storage }}"
else omit tpmstate0: >-
}} {{
ide: {'storage': hypervisor_cfg.storage, 'version': '2.0'}
ide0: "{{ boot_iso }},media=cdrom" if virtualization_tpm2_enabled | bool
ide1: "{{ rhel_iso + ',media=cdrom' if rhel_iso is defined and rhel_iso | length > 0 else omit }}" else omit
ide2: "{{ hypervisor_cfg.storage }}:cloudinit" }}
net: "{{ virtualization_proxmox_net }}" ide:
ipconfig: "{{ virtualization_proxmox_ipconfig }}" ide0: "{{ boot_iso }},media=cdrom"
nameservers: "{{ system_cfg.network.dns.servers if system_cfg.network.dns.servers | length else omit }}" ide1: "{{ rhel_iso + ',media=cdrom' if rhel_iso is defined and rhel_iso | length > 0 else omit }}"
searchdomains: "{{ system_cfg.network.dns.search if system_cfg.network.dns.search | length else omit }}" ide2: "{{ hypervisor_cfg.storage }}:cloudinit"
onboot: true net: "{{ virtualization_proxmox_net }}"
state: present ipconfig: "{{ virtualization_proxmox_ipconfig }}"
nameservers: "{{ system_cfg.network.dns.servers if system_cfg.network.dns.servers | length else omit }}"
searchdomains: "{{ system_cfg.network.dns.search if system_cfg.network.dns.search | length else omit }}"
onboot: true
state: present
no_log: true
- name: Start VM on Proxmox - name: Start VM on Proxmox
delegate_to: localhost community.proxmox.proxmox_kvm:
community.proxmox.proxmox_kvm: name: "{{ hostname }}"
api_host: "{{ hypervisor_cfg.url }}" vmid: "{{ system_cfg.id }}"
api_user: "{{ hypervisor_cfg.username }}" state: started
api_password: "{{ hypervisor_cfg.password }}" no_log: true
node: "{{ hypervisor_cfg.host }}" register: virtualization_proxmox_start_result
name: "{{ hostname }}"
vmid: "{{ system_cfg.id }}"
state: started
register: virtualization_proxmox_start_result
- name: Set VM created fact - name: Set VM created fact
ansible.builtin.set_fact: ansible.builtin.set_fact:
virtualization_vm_created_in_run: true virtualization_vm_created_in_run: true
when: virtualization_proxmox_start_result is defined and virtualization_proxmox_start_result.changed | bool when: virtualization_proxmox_start_result is defined and virtualization_proxmox_start_result.changed | bool

View File

@@ -10,101 +10,110 @@
loop: "{{ system_cfg.disks }}" loop: "{{ system_cfg.disks }}"
loop_control: loop_control:
label: "{{ item | to_json }}" label: "{{ item | to_json }}"
changed_when: false
- name: Create VM in vCenter - name: Deploy VM in vCenter
delegate_to: localhost delegate_to: localhost
vars: module_defaults:
virtualization_vmware_networks: >- community.vmware.vmware_guest:
{%- set ns = namespace(out=[]) -%} hostname: "{{ hypervisor_cfg.url }}"
{%- for iface in system_cfg.network.interfaces -%} username: "{{ hypervisor_cfg.username }}"
{%- set entry = {'name': iface.bridge, 'type': 'dhcp'} -%} password: "{{ hypervisor_cfg.password }}"
{%- if (iface.vlan | default('') | string | length) > 0 -%} validate_certs: "{{ hypervisor_cfg.certs | bool }}"
{%- set entry = entry | combine({'vlan': iface.vlan | int}) -%} datacenter: "{{ hypervisor_cfg.datacenter }}"
{%- endif -%} community.vmware.vmware_guest_tpm:
{%- set ns.out = ns.out + [entry] -%} hostname: "{{ hypervisor_cfg.url }}"
{%- endfor -%} username: "{{ hypervisor_cfg.username }}"
{{ ns.out }} password: "{{ hypervisor_cfg.password }}"
community.vmware.vmware_guest: validate_certs: "{{ hypervisor_cfg.certs | bool }}"
hostname: "{{ hypervisor_cfg.url }}" datacenter: "{{ hypervisor_cfg.datacenter }}"
username: "{{ hypervisor_cfg.username }}" vmware.vmware.vm_powerstate:
password: "{{ hypervisor_cfg.password }}" hostname: "{{ hypervisor_cfg.url }}"
validate_certs: "{{ hypervisor_cfg.certs | bool }}" username: "{{ hypervisor_cfg.username }}"
datacenter: "{{ hypervisor_cfg.datacenter }}" password: "{{ hypervisor_cfg.password }}"
cluster: "{{ hypervisor_cfg.cluster }}" validate_certs: "{{ hypervisor_cfg.certs | bool }}"
folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}" datacenter: "{{ hypervisor_cfg.datacenter }}"
name: "{{ hostname }}" block:
guest_id: otherLinux64Guest # community.vmware: full-featured guest management
annotation: | - name: Create VM in vCenter
{{ note if note is defined else '' }} vars:
state: "{{ 'poweredoff' if virtualization_tpm2_enabled | bool else 'poweredon' }}" virtualization_vmware_networks: >-
disk: "{{ virtualization_vmware_disks }}" {%- set ns = namespace(out=[]) -%}
hardware: {%- for iface in system_cfg.network.interfaces -%}
memory_mb: "{{ system_cfg.memory }}" {%- set entry = {'name': iface.bridge, 'type': 'dhcp'} -%}
num_cpus: "{{ system_cfg.cpus }}" {%- if (iface.vlan | default('') | string | length) > 0 -%}
boot_firmware: efi {%- set entry = entry | combine({'vlan': iface.vlan | int}) -%}
secure_boot: false {%- endif -%}
cdrom: >- {%- set ns.out = ns.out + [entry] -%}
{{ {%- endfor -%}
[ { {{ ns.out }}
"controller_number": 0, community.vmware.vmware_guest:
"unit_number": 0, cluster: "{{ hypervisor_cfg.cluster }}"
"controller_type": "sata", folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}"
"state": "present", name: "{{ hostname }}"
"type": "iso", # Generic guest ID — VMware auto-detects OS post-install
"iso_path": boot_iso guest_id: otherLinux64Guest
} ] annotation: |
+ {{ note if note is defined else '' }}
( [ { state: "{{ 'poweredoff' if virtualization_tpm2_enabled | bool else 'poweredon' }}"
"controller_number": 0, disk: "{{ virtualization_vmware_disks }}"
"unit_number": 1, hardware:
"controller_type": "sata", memory_mb: "{{ system_cfg.memory }}"
"state": "present", num_cpus: "{{ system_cfg.cpus }}"
"type": "iso", boot_firmware: efi
"iso_path": rhel_iso secure_boot: false
} ] if rhel_iso is defined and rhel_iso | length > 0 else [] ) cdrom: >-
}} {{
networks: "{{ virtualization_vmware_networks }}" [ {
register: virtualization_vmware_create_result "controller_number": 0,
"unit_number": 0,
"controller_type": "sata",
"state": "present",
"type": "iso",
"iso_path": boot_iso
} ]
+
( [ {
"controller_number": 0,
"unit_number": 1,
"controller_type": "sata",
"state": "present",
"type": "iso",
"iso_path": rhel_iso
} ] if rhel_iso is defined and rhel_iso | length > 0 else [] )
}}
networks: "{{ virtualization_vmware_networks }}"
no_log: true
register: virtualization_vmware_create_result
- name: Set VM created fact when VM was powered on during creation - name: Set VM created fact when VM was powered on during creation
ansible.builtin.set_fact: ansible.builtin.set_fact:
virtualization_vm_created_in_run: true virtualization_vm_created_in_run: true
when: when:
- virtualization_vmware_create_result is defined - virtualization_vmware_create_result is defined
- not virtualization_tpm2_enabled | bool - not virtualization_tpm2_enabled | bool
- virtualization_vmware_create_result.changed | bool - virtualization_vmware_create_result.changed | bool
- name: Ensure vTPM2 is enabled when required - name: Ensure vTPM2 is enabled when required
when: virtualization_tpm2_enabled | bool when: virtualization_tpm2_enabled | bool
delegate_to: localhost community.vmware.vmware_guest_tpm:
community.vmware.vmware_guest_tpm: folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}"
hostname: "{{ hypervisor_cfg.url }}" name: "{{ hostname }}"
username: "{{ hypervisor_cfg.username }}" state: present
password: "{{ hypervisor_cfg.password }}" no_log: true
validate_certs: "{{ hypervisor_cfg.certs | bool }}"
datacenter: "{{ hypervisor_cfg.datacenter }}"
folder: "{{ system_cfg.path if system_cfg.path | string | length > 0 else omit }}"
name: "{{ hostname }}"
state: present
- name: Start VM in vCenter # vmware.vmware: modern collection for power operations
when: virtualization_tpm2_enabled | bool - name: Start VM in vCenter
delegate_to: localhost when: virtualization_tpm2_enabled | bool
vmware.vmware.vm_powerstate: vmware.vmware.vm_powerstate:
hostname: "{{ hypervisor_cfg.url }}" name: "{{ hostname }}"
username: "{{ hypervisor_cfg.username }}" state: powered-on
password: "{{ hypervisor_cfg.password }}" no_log: true
validate_certs: "{{ hypervisor_cfg.certs | bool }}" register: virtualization_vmware_start_result
datacenter: "{{ hypervisor_cfg.datacenter }}"
name: "{{ hostname }}"
state: powered-on
register: virtualization_vmware_start_result
- name: Set VM created fact when VM was started separately (TPM2 case) - name: Set VM created fact when VM was started separately (TPM2 case)
ansible.builtin.set_fact: ansible.builtin.set_fact:
virtualization_vm_created_in_run: true virtualization_vm_created_in_run: true
when: when:
- virtualization_tpm2_enabled | bool - virtualization_tpm2_enabled | bool
- virtualization_vmware_start_result is defined - virtualization_vmware_start_result is defined
- virtualization_vmware_start_result.changed | bool - virtualization_vmware_start_result.changed | bool

View File

@@ -49,6 +49,16 @@
dest: /tmp/xen-{{ hostname }}.cfg dest: /tmp/xen-{{ hostname }}.cfg
mode: "0644" mode: "0644"
- name: Check if Xen VM already exists
delegate_to: localhost
ansible.builtin.command:
argv:
- xl
- list
register: virtualization_xen_pre_check
changed_when: false
failed_when: false
- name: Create Xen VM - name: Create Xen VM
delegate_to: localhost delegate_to: localhost
ansible.builtin.command: ansible.builtin.command:
@@ -58,8 +68,11 @@
- /tmp/xen-{{ hostname }}.cfg - /tmp/xen-{{ hostname }}.cfg
register: virtualization_xen_create_result register: virtualization_xen_create_result
changed_when: virtualization_xen_create_result.rc == 0 changed_when: virtualization_xen_create_result.rc == 0
when: >-
not (virtualization_xen_pre_check.stdout | default('')
is search('(?m)^' ~ (hostname | ansible.builtin.regex_escape) ~ '\\s+\\d+\\s'))
- name: Ensure VM is running - name: Verify VM is running
delegate_to: localhost delegate_to: localhost
ansible.builtin.command: ansible.builtin.command:
argv: argv:
@@ -67,13 +80,10 @@
- list - list
register: virtualization_xen_list_result register: virtualization_xen_list_result
changed_when: false changed_when: false
failed_when: false failed_when: >-
not (virtualization_xen_list_result.stdout | default('')
is search('(?m)^' ~ (hostname | ansible.builtin.regex_escape) ~ '\\s+\\d+\\s'))
- name: Set VM created fact - name: Set VM created fact
ansible.builtin.set_fact: ansible.builtin.set_fact:
virtualization_vm_created_in_run: true virtualization_vm_created_in_run: "{{ (virtualization_xen_create_result.rc | default(1)) == 0 }}"
when:
- virtualization_xen_list_result is defined
- >-
virtualization_xen_list_result.stdout | default('')
is search('(?m)^' ~ (hostname | ansible.builtin.regex_escape) ~ '\\s+\\d+\\s')

View File

@@ -13,7 +13,9 @@ network:
addresses: addresses:
- "{{ iface.ip }}/{{ iface.prefix }}" - "{{ iface.ip }}/{{ iface.prefix }}"
{% if iface.gateway | default('') | string | length %} {% if iface.gateway | default('') | string | length %}
gateway4: "{{ iface.gateway }}" routes:
- to: default
via: "{{ iface.gateway }}"
{% endif %} {% endif %}
{% else %} {% else %}
dhcp4: true dhcp4: true

View File

@@ -1,5 +1,5 @@
#cloud-config #cloud-config
hostname: "archiso" hostname: "{{ hostname }}"
ssh_pwauth: true ssh_pwauth: true
package_update: false package_update: false
package_upgrade: false package_upgrade: false

View File

@@ -1,15 +1,15 @@
<domain type='kvm'> <domain type='kvm'>
<name>{{ hostname }}</name> <name>{{ hostname }}</name>
<memory>{{ system_cfg.memory | int * 1024 }}</memory> <memory unit='KiB'>{{ system_cfg.memory | int * 1024 }}</memory>
{% if system_cfg.balloon is defined and system_cfg.balloon | int > 0 %}<currentMemory>{{ system_cfg.balloon | int * 1024 }}</currentMemory>{% endif %} {% if system_cfg.balloon is defined and system_cfg.balloon | int > 0 %}<currentMemory unit='KiB'>{{ system_cfg.balloon | int * 1024 }}</currentMemory>{% endif %}
<vcpu placement='static'>{{ system_cfg.cpus }}</vcpu> <vcpu placement='static'>{{ system_cfg.cpus }}</vcpu>
<os> <os>
<type arch='x86_64' machine="pc-q35-8.0">hvm</type> <type arch='x86_64' machine="{{ virtualization_libvirt_machine_type }}">hvm</type>
<bootmenu enable='no'/> <bootmenu enable='no'/>
<boot dev='hd'/> <boot dev='hd'/>
<boot dev='cdrom'/> <boot dev='cdrom'/>
<loader readonly="yes" type="pflash">/usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd</loader> <loader readonly="yes" type="pflash">{{ virtualization_libvirt_ovmf_code }}</loader>
<nvram template="/usr/share/edk2/x64/OVMF_VARS.4m.fd"/> <nvram template="{{ virtualization_libvirt_ovmf_vars }}"/>
</os> </os>
<features> <features>
<acpi/> <acpi/>
@@ -33,17 +33,20 @@
<driver name="qemu" type="raw"/> <driver name="qemu" type="raw"/>
<source file="{{ boot_iso }}"/> <source file="{{ boot_iso }}"/>
<target dev="sda" bus="sata"/> <target dev="sda" bus="sata"/>
<readonly/>
</disk> </disk>
<disk type="file" device="cdrom"> <disk type="file" device="cdrom">
<driver name="qemu" type="raw"/> <driver name="qemu" type="raw"/>
<source file="{{ virtualization_libvirt_cloudinit_path }}"/> <source file="{{ virtualization_libvirt_cloudinit_path }}"/>
<target dev="sdb" bus="sata"/> <target dev="sdb" bus="sata"/>
<readonly/>
</disk> </disk>
{% if rhel_iso is defined and rhel_iso | length > 0 %} {% if rhel_iso is defined and rhel_iso | length > 0 %}
<disk type="file" device="cdrom"> <disk type="file" device="cdrom">
<driver name="qemu" type="raw"/> <driver name="qemu" type="raw"/>
<source file="{{ rhel_iso }}"/> <source file="{{ rhel_iso }}"/>
<target dev="sdc" bus="sata"/> <target dev="sdc" bus="sata"/>
<readonly/>
</disk> </disk>
{% endif %} {% endif %}
{% for iface in system_cfg.network.interfaces %} {% for iface in system_cfg.network.interfaces %}

View File

@@ -12,7 +12,7 @@ disk = [
] ]
vif = [ vif = [
{%- for iface in system_cfg.network.interfaces -%} {%- for iface in system_cfg.network.interfaces -%}
'bridge={{ iface.bridge }},model=e1000'{% if not loop.last %}, {% endif %} 'bridge={{ iface.bridge }},model=virtio'{% if not loop.last %}, {% endif %}
{%- endfor -%} {%- endfor -%}
] ]
boot = "{{ 'dc' if xen_installer_media_enabled | bool else 'c' }}" boot = "{{ 'dc' if xen_installer_media_enabled | bool else 'c' }}"